Package: property-graph
python property graph (pypg)Object-oriented descriptor properties designed for capturing complex data-graphs and rich metadata from objects and types.Motivationpypg provides a flexible and terse framework for expressing object schemas, initialization, de/serializiation, and declarative behavior.ExampleConsider a directed acyclic graph (DAG) such as:root
 /   \
1A   1B
 \  /  \
  2A   2B
   \   /
    endnote that 1B is referenced by both 2A and 2B. Naively serializing this object will result in duplication of this data and any upstream nodes, and, upon de-serialiation, the referential integrity will be lost.To combat this issue, one can transform the data using adjacency lists or comparison of other unique identifiers to establish relationships.Using pypg, the DAG above can be expressed like so:frompypgimportProperty,PropertyClassfrompypg.transcodeimportencode,decodeclassNode(PropertyClass):upstream=Property[list["Node"]]()root=Node()layer_1=[Node(upstream=[root]),Node(upstream=[root])]layer_2=[Node(upstream=layer_1),Node(upstream=[layer_1[1]])]terminal=Node(upstream=layer_2)This structure can be serialized and reconstructed without duplication of object instances, and all identity relationships are preserved.nodes=[terminal,*layer_2,*layer_1,root]serialized_nodes=encode(nodes)copy_nodes=decode(serialized_nodes)terminal_copy,l2a,l2b,l1a,l1b,root_copy=copy_nodesassertterminal_copy.upstream[0]isl2aassertterminal_copy.upstream[1]isl2bassertl2a.upstream[0]isl1aassertl2a.upstream[1]isl1bassertl2b.upstream[0]isl1bassertl1a.upstream[0]isroot_copyassertl1b.upstream[0]isroot_copyNote that in this case it is not necessary to collect all the objects for encoding, the terminal node is sufficient to capture the entire graph.serialized=encode(terminal)terminal_copy=decode(serialized)l2a,l2b=terminal_copy.upstreaml1a,l1b=l2a.upstream(also_l1b,)=l2b.upstreamassertl1bisalso_l1b(root_copy,)=l1a.upstream(also_root_copy,)=l1b.upstreamassertroot_copyisalso_root_copyWhy not dataclasses?Aside from cases when object-identity-fidelity is important, dataclasses impose limitations. For example:fromdataclassesimportdataclass@dataclassclassBase:a:int=0@dataclassclassDerived(Base):b:int@dataclass^^^^^^^^^File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/dataclasses.py", line 1220, in dataclassreturn wrap(cls)^^^^^^^^^File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/dataclasses.py", line 1210, in wrapreturn _process_class(cls, init, repr, eq, order, unsafe_hash,^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/dataclasses.py", line 1027, in _process_class_init_fn(all_init_fields,File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/dataclasses.py", line 545, in _init_fnraise TypeError(f'non-default argument {f.name!r} 'TypeError: non-default argument 'b' follows default argumentpython-BaseExceptionIf a base class declares a default field, any subsequent fields must also have default values. This violation of the open/closed principle makes dataclasses an unsuitable choice for extendable objects.Why not JSON Schema?JSON Schema requires a separate second representation of an object just to describe it. This requires 2 edits to be made for any 1 change, increasing development effort and potential for error.Why descriptors?Python descriptors provide a powerful mechanism for encoding type-information and customizing data-handling behaviors. Descriptor classes can be used to extend the semantics of working with object fields with very few limitations. A few examples of those contained in pypg are:the observer patternvalue validationunits of measureBy composing decorator-like objects, here called Traits, Property objects provide an expressive but powerful syntax for defining not just data, but also runtime-behaviors that should occur with data access semantics. For more examples, see:https://github.com/mattefowler/pypg/blob/main/tests/test_traits.pyhttps://github.com/mattefowler/pypg/blob/main/tests/test_observable.pyhttps://github.com/mattefowler/pypg/blob/main/tests/test_overridable.py