casework / CASE-Mapping-Python

Apache License 2.0
0 stars 3 forks source link

Type cardinalities of `CallFacet` parameters #46

Closed ajnelson-nist closed 4 months ago

ajnelson-nist commented 4 months ago

This Pull Request is posted for discussion. I'm fine with it being merged, but please do not merge with less than two GitHub approvals.

The first patch in this PR enacts the following change:

-        call_participant=None,
-        call_from=None,
-        call_to=None,
+        call_participant: Union[None, ObservableObject, List[ObservableObject]] = None,
+        call_from: Union[None, ObservableObject] = None,
+        call_to: Union[None, ObservableObject, List[ObservableObject]] = None,

Here is a breakdown of the effects:

 ...
-        call_from=None,
 ...
+        call_from: Union[None, ObservableObject] = None,
 ...

This denotes call_from is an optional singleton-valued parameter.

-        call_participant=None,
 ...
-        call_to=None,
+        call_participant: Union[None, ObservableObject, List[ObservableObject]] = None,
 ...
+        call_to: Union[None, ObservableObject, List[ObservableObject]] = None,

This denotes call_participant and call_to are optional parameters, which can be a singleton ObservableObject or a list of ObservableObjects.

One side-effect of this patch benefitting type safety of the generated graph objects is that the type system enforces that ObservableObjects are used in these fields. That is, it would be an error flagged by the type system to pass a uco.identity.Person object here. (While the ontology currently would permit an object to be multi-typed as Person and ObservableObject, there is a coming proposal to disallow this and several other conflations. For example, UCO should never accidentally type a node as Person and File; but, how to implement this in SHACL and/or OWL is taking some thought.)

A side-effect of this patch is that type-checking will fail on any downstream code that tries passing a raw JSON object. I.e. this code currently in example.py is fine ...

account_object_1 = uco.observable.ObservableObject()
call_facet = uco.observable.FacetCall(
    # ...
    call_from=account_object_1,
    # ...
)

... and, this would also be fine (classes inlined because I realized they aren't in this code base yet) ...

class Account(uco.observable.ObservableObject): pass
class PhoneAccount(Account): pass
account_object_1 = uco.observable.PhoneAccount()
call_facet = uco.observable.FacetCall(
    # ...
    call_from=account_object_1,
    # ...
)

... but, someone bypassing the objects hierarchy in this code base would get an error from static type review on trying this:

call_facet = uco.observable.FacetCall(
    # ...
    call_from={
        "@id": "kb:e74ab5f6-cf70-436b-9a58-bed50cff7e79",
        "@type": "uco-observable:PhoneAccount",
    }
    # ...
)

The error from the current type-checking in this code base is:

example.py:455: error: Argument "call_from" to "FacetCall" has incompatible type "dict[str, str]"; expected "Optional[ObservableObject]"  [arg-type]

I'm uncertain how likely this last use case is to occur. But, if it's something this code base should support, then this patch could keep the type system happy ...

-        call_participant: Union[None, ObservableObject, List[ObservableObject]] = None,
-        call_from: Union[None, ObservableObject] = None,
-        call_to: Union[None, ObservableObject, List[ObservableObject]] = None,
+        call_participant: Union[None, dict, List[dict]] = None,
+        call_from: Union[None, dict] = None,
+        call_to: Union[None, dict, List[dict]] = None,

... but I don't think it's possible to then make use of the Python class hierarchy for type safety. E.g. that Person graph objct could go in to the Python code, and would have to be caught by SHACL validation to realize it's not an ObservableObject.

In summary: If the type system is to be used to check cardinalities, a decision will have to also be made on type enforcement in methods' parameters.

If the goal of this code base is to catch errors in bindings before run time, the type system is an option. If instead the goal is to not generate incorrect graphs, asserts in object-initialization, list-appends, or property setter methods could meet the cardinality enforcement goal and bypass Python typing questions.

ajnelson-nist commented 4 months ago

(This PR is a follow-on from review of #45 .)

fabrizio-turchi commented 4 months ago

@ajnelson-nist everything is clear and elegant, the only doubt concerns the following code:

-        call_participant: Union[None, ObservableObject, List[ObservableObject]] = None,
...
+        call_participant: Union[None, dict, List[dict]] = None,
...

The dict ObservableObject has the method get_id() while the generic dict may not have it so at runtime it would raise an error. It might be fine anyway, the caller will be in charge to use the right parameter.

ajnelson-nist commented 4 months ago

@ajnelson-nist everything is clear and elegant, the only doubt concerns the following code:

-        call_participant: Union[None, ObservableObject, List[ObservableObject]] = None,
...
+        call_participant: Union[None, dict, List[dict]] = None,
...

The dict ObservableObject has the method get_id() while the generic dict may not have it so at runtime it would raise an error. It might be fine anyway, the caller will be in charge to use the right parameter.

@fabrizio-turchi , thank you for the feedback.

Your remark about get_id(), leading to recalling other methods special to the case_mappings class hierarchy that specializes dict, further confirms to me that this code base should encourage using its specialized classes rather than permitting plain dicts.

If you agree, please leave an approving Review (no description text needed).