Module pyracmon.graph

Expand source code
from .graph import new_graph, Graph
from .spec import GraphSpec
from .serialize import S
from .template import GraphTemplate
from .identify import IdentifyPolicy


__all__ = [
    "new_graph",
    "S",
    "GraphSpec",
    "Graph",
    "GraphTemplate",
    "IdentifyPolicy",
]

Sub-modules

pyracmon.graph.graph

This module exports types representing graphs.

pyracmon.graph.identify
pyracmon.graph.protocol
pyracmon.graph.schema

This module provides the way to generate schema of the graph after being serialized …

pyracmon.graph.serialize
pyracmon.graph.spec

This module provides a type which contains objects to control how graphs work.

pyracmon.graph.stub

from collections.abc import Iterator from typing import Any, Optional, Generic, TypeVar, ForwardRef, TYPE_CHECKING, cast from typing_extensions import …

pyracmon.graph.template

This module provides a type specifying graph structure.

pyracmon.graph.typing

Functions

def new_graph(template: GraphTemplate, *bases: Union[GraphGraphView]) ‑> Graph

Create a graph from a template.

Use this function instead of invoking constructor directly.

Args

template
A template of a graph.
bases
Other graphs whose nodes are appended to created graph.

Returns

Created graph.

Expand source code
def new_graph(template: GraphTemplate, *bases: Union[Graph, GraphView]) -> Graph:
    """
    Create a graph from a template.

    Use this function instead of invoking constructor directly.

    Args:
        template: A template of a graph.
        bases: Other graphs whose nodes are appended to created graph.
    Returns:
        Created graph.
    """
    graph = Graph(template)

    for b in bases:
        graph += b

    return graph

Classes

class Graph (template: GraphTemplate)

This class represents a graph composed of tree-structured node containers.

The structure is determined by GraphTemplate. Use new_graph() Instead of constructor to create new graph instance.

template = GraphSpac().new_template(
    a = (int, lambda x:x),
    b = (str, lambda x:x),
    c = (str, lambda x:x),
)
template.a << template.b << template.c
graph = new_graph(template)

In above code, a graph which has 3 properties ( a b c ) and a structure where a is parent of b and b is parent of c is created.

append ( replace ) is a method to store entities in the graph with tying them each other according to the structure. Entites are encapsulated by Node which can have an edge to parent node.

graph.append(a=1, b="a", c="x").append(a=2, b="b", c="y")

In append, entities are first sorted in descending order, and then:

  • Search a node whose entity is identical to the first entity from the corresponding node container.
    • If found, new node is not created and the identical node is set to next parent.
    • Otherwise, new node is appended and it is set to next parent.
  • Apply this to following entities in order. A difference is that identical node is searched from the sequence of parents in the session.

In example here, the identification is done by entity value itself ( lambda x:x ). Next code is the example where identical nodes are found.

graph.append(a=1, b="a", c="z").append(a=2, b="c", c="y")

In the first append, a and b has its identical node and a is identical in the second. c in the second one is not identical to any node because parent node b="c" is already added as new node.

Due to the identification mechanism, entity relationships in the graph is guaranteed after repeating append .

Expand source code
class Graph:
    """
    This class represents a graph composed of tree-structured node containers.

    The structure is determined by `GraphTemplate`. Use `new_graph` Instead of constructor to create new graph instance.

    ```python
    template = GraphSpac().new_template(
        a = (int, lambda x:x),
        b = (str, lambda x:x),
        c = (str, lambda x:x),
    )
    template.a << template.b << template.c
    graph = new_graph(template)
    ```

    In above code, a graph which has 3 properties ( `a` `b` `c` ) and a structure where `a` is parent of `b` and `b` is parent of `c` is created.

    `append` ( `replace` ) is a method to store entities in the graph with tying them each other according to the structure.
    Entites are encapsulated by `Node` which can have an edge to parent node.

    ```python
    graph.append(a=1, b="a", c="x").append(a=2, b="b", c="y")
    ```

    In `append`, entities are first sorted in descending order, and then:

    - Search a node whose entity is *identical* to the first entity from the corresponding node container.
        - If found, new node is not created and the *identical* node is set to next parent.
        - Otherwise, new node is appended and it is set to next parent.
    - Apply this to following entities in order. A difference is that *identical* node is searched from the sequence of parents in the session.

    In example here, the identification is done by entity value itself ( `lambda x:x` ). Next code is the example where *identical* nodes are found.

    ```python
    graph.append(a=1, b="a", c="z").append(a=2, b="c", c="y")
    ```

    In the first `append`, `a` and `b` has its *identical* node and `a` is *identical* in the second.
    `c` in the second one is not *identical* to any node because parent node `b="c"` is already added as new node.

    Due to the identification mechanism, entity relationships in the graph is guaranteed after repeating `append` .
    """
    def __init__(self, template: GraphTemplate):
        #: Graph template.
        self.template: GraphTemplate = template
        #: A `dict` containing node containers by their names.
        self.containers: dict[str, NodeContainer] = {p.name:self._to_container(p) for p in template}
        self._view = None

    def _to_container(self, prop: GraphTemplate.Property) -> 'NodeContainer':
        if isinstance(prop.kind, GraphTemplate):
            return _GraphNodeContainer(prop)
        else:
            return NodeContainer(prop)

    def _container_of(self, prop: GraphTemplate.Property) -> Optional['NodeContainer']:
        candidates = [c for c in self.containers.values() if c.prop.is_compatible(prop)]
        if len(candidates) > 1:
            raise ValueError(f"Container can't be determined from property '{prop.name}'.")
        return candidates[0] if candidates else None

    def __add__(self, another: Union[Self, GraphView]) -> 'Graph':
        """
        Create new graph by adding this graph and another graph.

        New graph has the same template as this graph's.
        On the other hand, because this method depends on `__iadd__()`, another graph must not have the same template.

        Args:
            another: Graph or its view.
        Returns:
            Created graph.
        """
        graph = Graph(self.template)

        graph += self
        graph += another

        return graph

    def __iadd__(self, another: Union[Self, GraphView]) -> Self:
        """
        Append nodes from another graph.

        Nodes of another graph are traversed from its root and appended to compatible containers each other.

        Args:
            another: Graph or its view.
        Returns:
            This graph.
        """
        graph = another if isinstance(another, Graph) else another()

        def add(n: Node, anc: dict[str, list[Node]]):
            c = self._container_of(n.prop)
            if c:
                c.append(n.entity, anc)
            for ch_ in n.children.values():
                for m in ch_.nodes:
                    add(m, anc.copy())

        for c_ in graph.roots:
            for n_ in c_.nodes:
                add(n_, {})

        return self

    @property
    def roots(self) -> Iterable['NodeContainer']:
        """
        Returns root node containers.
        """
        return filter(lambda c: c.prop.parent is None, self.containers.values())

    @property
    def view(self) -> GraphView:
        """
        Returns an unmodifiable view of this graph.

        The view object works as the accessor to graph nodes.

        ```python
        >>> template = GraphSpac().new_template(a=int, b=str, c=str)
        >>> template.a << template.b
        >>> graph = new_graph(template)
        >>> view = graph.view
        >>> assert view() is graph                        # invocation
        >>> assert view.a is graph.containers["a"].view   # attribute
        >>> assert [c().name for c in view] == ["a", "c"] # iteration
        ```
        """
        if self._view is None:
            graph = self
            class _GraphView:
                def __call__(self) -> Graph:
                    """Returns the greph of this view."""
                    return graph
                def __iter__(self) -> Iterator[tuple[str, ContainerView[NodeContainer]]]:
                    """Iterates views of root containers."""
                    return map(lambda c: (c.name, c.view), filter(lambda c: c.prop.parent is None, graph.containers.values()))
                def __getattr__(self, name: str) -> ContainerView:
                    """Returns a view of a container of the name."""
                    return graph.containers[name].view
            self._view = _GraphView()
        return self._view

    def _append(self, to_replace: bool, entities: dict[str, Any]) -> Self:
        props = [p for p in self.template if p.name in entities]

        filtered = set()
        for p in props:
            if (p.parent is None) or (p.parent.name not in entities) or (p.parent.name in filtered):
                if p.entity_filter is None or p.entity_filter(entities[p.name]):
                    filtered.add(p.name)

        ancestors = {}
        for k in [p.name for p in props if p.name in filtered]:
            self.containers[k].append(entities[k], ancestors, to_replace)

        return self

    def append(self, **entities: Any) -> Self:
        """
        Append entities with associated property names.

        Args:
            entities: Entities keyed with associated property names.
        Returns:
            This graph.
        """
        return self._append(False, entities)

    def replace(self, **entities: Any) -> Self:
        """
        Works similarly to `append`, but entities of identical nodes are replaced with given entities.

        Args:
            entities: Entities keyed with associated property names.
        Returns:
            This graph.
        """
        return self._append(True, entities)

Instance variables

var containers

A dict containing node containers by their names.

var roots : collections.abc.Iterable['NodeContainer']

Returns root node containers.

Expand source code
@property
def roots(self) -> Iterable['NodeContainer']:
    """
    Returns root node containers.
    """
    return filter(lambda c: c.prop.parent is None, self.containers.values())
var template

Graph template.

var viewGraphView

Returns an unmodifiable view of this graph.

The view object works as the accessor to graph nodes.

>>> template = GraphSpac().new_template(a=int, b=str, c=str)
>>> template.a << template.b
>>> graph = new_graph(template)
>>> view = graph.view
>>> assert view() is graph                        # invocation
>>> assert view.a is graph.containers["a"].view   # attribute
>>> assert [c().name for c in view] == ["a", "c"] # iteration
Expand source code
@property
def view(self) -> GraphView:
    """
    Returns an unmodifiable view of this graph.

    The view object works as the accessor to graph nodes.

    ```python
    >>> template = GraphSpac().new_template(a=int, b=str, c=str)
    >>> template.a << template.b
    >>> graph = new_graph(template)
    >>> view = graph.view
    >>> assert view() is graph                        # invocation
    >>> assert view.a is graph.containers["a"].view   # attribute
    >>> assert [c().name for c in view] == ["a", "c"] # iteration
    ```
    """
    if self._view is None:
        graph = self
        class _GraphView:
            def __call__(self) -> Graph:
                """Returns the greph of this view."""
                return graph
            def __iter__(self) -> Iterator[tuple[str, ContainerView[NodeContainer]]]:
                """Iterates views of root containers."""
                return map(lambda c: (c.name, c.view), filter(lambda c: c.prop.parent is None, graph.containers.values()))
            def __getattr__(self, name: str) -> ContainerView:
                """Returns a view of a container of the name."""
                return graph.containers[name].view
        self._view = _GraphView()
    return self._view

Methods

def append(self, **entities: Any) ‑> typing_extensions.Self

Append entities with associated property names.

Args

entities
Entities keyed with associated property names.

Returns

This graph.

Expand source code
def append(self, **entities: Any) -> Self:
    """
    Append entities with associated property names.

    Args:
        entities: Entities keyed with associated property names.
    Returns:
        This graph.
    """
    return self._append(False, entities)
def replace(self, **entities: Any) ‑> typing_extensions.Self

Works similarly to append, but entities of identical nodes are replaced with given entities.

Args

entities
Entities keyed with associated property names.

Returns

This graph.

Expand source code
def replace(self, **entities: Any) -> Self:
    """
    Works similarly to `append`, but entities of identical nodes are replaced with given entities.

    Args:
        entities: Entities keyed with associated property names.
    Returns:
        This graph.
    """
    return self._append(True, entities)
class GraphSpec (identifiers: Optional[list[tuple[type, typing.Callable[[typing.Any], typing.Any]]]] = None, entity_filters: Optional[list[tuple[type, typing.Callable[[typing.Any], bool]]]] = None, serializers: Optional[list[tuple[type, typing.Callable[[ForwardRef('NodeContext')], typing.Any]]]] = None)

This class contains the specifications of graph which control various behaviors in the lifecycles of graphs.

3 kinds of functions are the core of graph behaviors: identifier, entity filter and serializer .

Identifier and entity filter are used when appending values into graph. Identifier is a function to get a value used for the identification of graph entity. See Graph to know how this works. Entity fliter is a function to determine whether the entity should be appended to a graph or not. If False is returned for an entity, it is just ignored.

See pyracmon.graph.serialize to know the detail of serializer.

Each of them is bound to a type on registration to this and it affects nodes whose property type conforms to the type .

Expand source code
class GraphSpec:
    """
    This class contains the specifications of graph which control various behaviors in the lifecycles of graphs.

    3 kinds of functions are the core of graph behaviors: *identifier*, *entity filter* and *serializer* .

    *Identifier* and *entity filter* are used when appending values into graph.
    *Identifier* is a function to get a value used for the identification of graph entity. See `Graph` to know how this works.
    *Entity fliter* is a function to determine whether the entity should be appended to a graph or not.
    If `False` is returned for an entity, it is just ignored.

    See `pyracmon.graph.serialize` to know the detail of *serializer*.

    Each of them is bound to a `type` on registration to this and it affects nodes whose property type conforms to the `type` .
    """
    def __init__(
        self,
        identifiers: Optional[list[tuple[type, Identifier]]] = None,
        entity_filters: Optional[list[tuple[type, EntityFilter]]] = None,
        serializers: Optional[list[tuple[type, Serializer]]] = None,
    ):
        #: A list of pairs of type and *identifier*.
        self.identifiers: list[tuple[type, Identifier]] = identifiers or []
        #: A list of pairs of type and *entity_filter*.
        self.entity_filters: list[tuple[type, EntityFilter]] = entity_filters or []
        #: A list of pairs of type and *serializer*.
        self.serializers: list[tuple[type, Serializer]] = serializers or []

    def _get_inherited(self, holder: list[tuple[type, T]], t: type) -> Optional[T]:
        if not isinstance(t, type):
            return None
        return next(map(lambda x:x[1], filter(lambda x:issubtype(t, x[0]), holder)), None)

    def get_identifier(self, t: type) -> Optional[Callable[[Any], Any]]:
        """
        Returns the most appropriate identifier for a type.
        
        Args:
            t: Type of an entity.
        Returns:
            Identifier if exists.
        """
        return self._get_inherited(self.identifiers, t)

    def get_entity_filter(self, t: type) -> Optional[Callable[[Any], bool]]:
        """
        Returns the most appropriate entity filter for a type.
        
        Args:
            t: Type of an entity.
        Returns:
            Entity filter if exists.
        """
        return self._get_inherited(self.entity_filters, t)

    def find_serializers(self, t: type) -> list[Serializer]:
        """
        Returns a list of serializers applicable to a type.
        
        Args:
            t: Type of an entity.
        Returns:
            Serializers found.
        """
        if not isinstance(t, type):
            return []
        return list(map(lambda x:x[1], filter(lambda x:issubtype(t, x[0]), self.serializers[::-1])))

    def add_identifier(self, c: type, f: Callable[[Any], Any]) -> Self:
        """
        Register an identifier with a type.

        Args:
            c: A type bound to the identifier.
            f: An identifier function.
        Returns:
            This instance.
        """
        self.identifiers[0:0] = [(c, f)]
        return self

    def add_entity_filter(self, c: type, f: Callable[[Any], bool]) -> Self:
        """
        Register an entity filter with a type.

        Args:
            c: A type bound to the identifier.
            f: An entity filter function.
        Returns:
            This instance.
        """
        self.entity_filters[0:0] = [(c, f)]
        return self

    def add_serializer(self, c: type, f: Union[Serializer, NodeSerializer]) -> Self:
        """
        Register a serializer with a type.

        Args:
            c: A type bound to the identifier.
            f: A serializer function.
        Returns:
            This instance.
        """
        if isinstance(f, NodeSerializer):
            f = f.serializer
        self.serializers[0:0] = [(c, f)]
        return self

    def _make_policy(self, t: type, f: Union[IdentifyPolicy, Callable[[Any], Any], None]) -> IdentifyPolicy:
        f = f or self.get_identifier(t)

        if isinstance(f, IdentifyPolicy):
            return f
        elif callable(f):
            return HierarchicalPolicy(f)
        else:
            return neverPolicy()

    def _get_property_definition(self, definition: Union[
        TemplateProperty,
        type,
        GraphTemplate,
    ]) -> tuple[TypeDef, IdentifyPolicy, Optional[EntityFilter]]:
        if isinstance(definition, GraphTemplate):
            return definition, neverPolicy(), None
        elif isinstance(definition, type):
            return definition, self._make_policy(definition, None), self.get_entity_filter(definition)
        elif isinstance(definition, tuple):
            # python < 3.10
            if len(definition) == 3:
                kind, identifier, entity_filter = definition
            elif len(definition) == 2:
                kind, identifier, entity_filter = definition + (None,)
            elif len(definition) == 1:
                kind, identifier, entity_filter = definition + (None, None)
            elif len(definition) == 0:
                kind, identifier, entity_filter = (object, None, None)
            else:
                raise ValueError(f"Invalid value was found in keyword arguments of new_template().")
            # python >= 3.10
            #match definition:
            #    case (k, ident, ef):
            #        kind = k; identifier = ident; entity_filter = ef
            #    case (k, ident):
            #        kind = k; identifier = ident; entity_filter = None
            #    case (k,):
            #        kind = k; identifier = None; entity_filter = None
            #    case ():
            #        kind = object; identifier = None; entity_filter = None
            #    case _:
            #        raise ValueError(f"Invalid value was found in keyword arguments of new_template().")
            return kind, self._make_policy(kind, identifier), entity_filter or self.get_entity_filter(kind)
        else:
            raise ValueError(f"Invalid value was found in keyword arguments of new_template().")

    def new_template(self, *bases: GraphTemplate, **properties: Union[TemplateProperty, type, GraphTemplate]) -> GraphTemplate:
        """
        Creates a graph template with definitions of template properties.

        Each keyword argument corresponds to a template property where the key is proprety name and value is property definition.

        Property definition can be a `type` object or a tuple of at most 3 values.
        The former is the equivalent to a tuple which contains the `type` object alone.
        Values in the tuple are interpreted into following attributes in order.

        - The kind of property which indicates a type of entity.
        - *Identifier* of the property.
        - *Entity filter* of the property. 

        Omitted values are completed with registered items in this object.

        ```python
        template = GraphSpac().new_template(
            a = int,
            b = (str, lambda x:x),
            c = (str, lambda x:x, lambda x:len(x)>5),
        )
        ```

        Args:
            bases: Base templates whose properties and relations are merged into new template.
            properties: Definitions of template properties.
        Returns:
            Created graph template.
        """
        base = sum(bases, GraphTemplate([]))

        return base + GraphTemplate([(n, *self._get_property_definition(d)) for n, d in properties.items()])

    def to_dict(self, graph: GraphView, _params_: dict[str, dict[str, Any]] = {}, **settings: NodeSerializer) -> dict[str, Any]:
        """
        Serialize a graph into a `dict` .

        Only nodes whose names appear in keys of `settings` are serialized into the result.
        Each `NodeSerializer` object can be built by factory methods on `pyracmon.graph.serialize.S`.

        ```python
        GraphSpec().to_dict(
            graph,
            a = S.of(),
            b = S.name("B"),
        )
        ```

        Args:
            graph: A view of the graph.
            _params_: Parameters passed to `SerializationContext` and used by *serializer*s.
            settings: `NodeSerializer` for each property.
        Returns:
            Serialization result.
        """
        return SerializationContext(settings, self.find_serializers, _params_).execute(graph)

    def to_schema(self, template: GraphTemplate, **settings: NodeSerializer) -> GraphSchema:
        """
        Creates `GraphSchema` representing the structure of serialization result under given settings.

        Args:
            template: Template of a graph.
            settings: `NodeSerializer` for each property.
        Returns:
            Schema of serialization result.
        """
        return GraphSchema(self, template, **settings)

Subclasses

Instance variables

var entity_filters

A list of pairs of type and entity_filter.

var identifiers

A list of pairs of type and identifier.

var serializers

A list of pairs of type and serializer.

Methods

def add_entity_filter(self, c: type, f: Callable[[Any], bool]) ‑> typing_extensions.Self

Register an entity filter with a type.

Args

c
A type bound to the identifier.
f
An entity filter function.

Returns

This instance.

Expand source code
def add_entity_filter(self, c: type, f: Callable[[Any], bool]) -> Self:
    """
    Register an entity filter with a type.

    Args:
        c: A type bound to the identifier.
        f: An entity filter function.
    Returns:
        This instance.
    """
    self.entity_filters[0:0] = [(c, f)]
    return self
def add_identifier(self, c: type, f: Callable[[Any], Any]) ‑> typing_extensions.Self

Register an identifier with a type.

Args

c
A type bound to the identifier.
f
An identifier function.

Returns

This instance.

Expand source code
def add_identifier(self, c: type, f: Callable[[Any], Any]) -> Self:
    """
    Register an identifier with a type.

    Args:
        c: A type bound to the identifier.
        f: An identifier function.
    Returns:
        This instance.
    """
    self.identifiers[0:0] = [(c, f)]
    return self
def add_serializer(self, c: type, f: Union[Callable[[ForwardRef('NodeContext')], Any], NodeSerializer]) ‑> typing_extensions.Self

Register a serializer with a type.

Args

c
A type bound to the identifier.
f
A serializer function.

Returns

This instance.

Expand source code
def add_serializer(self, c: type, f: Union[Serializer, NodeSerializer]) -> Self:
    """
    Register a serializer with a type.

    Args:
        c: A type bound to the identifier.
        f: A serializer function.
    Returns:
        This instance.
    """
    if isinstance(f, NodeSerializer):
        f = f.serializer
    self.serializers[0:0] = [(c, f)]
    return self
def find_serializers(self, t: type) ‑> list[typing.Callable[[NodeContext], typing.Any]]

Returns a list of serializers applicable to a type.

Args

t
Type of an entity.

Returns

Serializers found.

Expand source code
def find_serializers(self, t: type) -> list[Serializer]:
    """
    Returns a list of serializers applicable to a type.
    
    Args:
        t: Type of an entity.
    Returns:
        Serializers found.
    """
    if not isinstance(t, type):
        return []
    return list(map(lambda x:x[1], filter(lambda x:issubtype(t, x[0]), self.serializers[::-1])))
def get_entity_filter(self, t: type) ‑> Optional[Callable[[Any], bool]]

Returns the most appropriate entity filter for a type.

Args

t
Type of an entity.

Returns

Entity filter if exists.

Expand source code
def get_entity_filter(self, t: type) -> Optional[Callable[[Any], bool]]:
    """
    Returns the most appropriate entity filter for a type.
    
    Args:
        t: Type of an entity.
    Returns:
        Entity filter if exists.
    """
    return self._get_inherited(self.entity_filters, t)
def get_identifier(self, t: type) ‑> Optional[Callable[[Any], Any]]

Returns the most appropriate identifier for a type.

Args

t
Type of an entity.

Returns

Identifier if exists.

Expand source code
def get_identifier(self, t: type) -> Optional[Callable[[Any], Any]]:
    """
    Returns the most appropriate identifier for a type.
    
    Args:
        t: Type of an entity.
    Returns:
        Identifier if exists.
    """
    return self._get_inherited(self.identifiers, t)
def new_template(self, *bases: GraphTemplate, **properties: Union[type, tuple[()], tuple[type], tuple[type, Optional[Callable[[Any], Any]]], tuple[type, Optional[Callable[[Any], Any]], Optional[Callable[[Any], bool]]], GraphTemplate]) ‑> GraphTemplate

Creates a graph template with definitions of template properties.

Each keyword argument corresponds to a template property where the key is proprety name and value is property definition.

Property definition can be a type object or a tuple of at most 3 values. The former is the equivalent to a tuple which contains the type object alone. Values in the tuple are interpreted into following attributes in order.

  • The kind of property which indicates a type of entity.
  • Identifier of the property.
  • Entity filter of the property.

Omitted values are completed with registered items in this object.

template = GraphSpac().new_template(
    a = int,
    b = (str, lambda x:x),
    c = (str, lambda x:x, lambda x:len(x)>5),
)

Args

bases
Base templates whose properties and relations are merged into new template.
properties
Definitions of template properties.

Returns

Created graph template.

Expand source code
def new_template(self, *bases: GraphTemplate, **properties: Union[TemplateProperty, type, GraphTemplate]) -> GraphTemplate:
    """
    Creates a graph template with definitions of template properties.

    Each keyword argument corresponds to a template property where the key is proprety name and value is property definition.

    Property definition can be a `type` object or a tuple of at most 3 values.
    The former is the equivalent to a tuple which contains the `type` object alone.
    Values in the tuple are interpreted into following attributes in order.

    - The kind of property which indicates a type of entity.
    - *Identifier* of the property.
    - *Entity filter* of the property. 

    Omitted values are completed with registered items in this object.

    ```python
    template = GraphSpac().new_template(
        a = int,
        b = (str, lambda x:x),
        c = (str, lambda x:x, lambda x:len(x)>5),
    )
    ```

    Args:
        bases: Base templates whose properties and relations are merged into new template.
        properties: Definitions of template properties.
    Returns:
        Created graph template.
    """
    base = sum(bases, GraphTemplate([]))

    return base + GraphTemplate([(n, *self._get_property_definition(d)) for n, d in properties.items()])
def to_dict(self, graph: GraphView, **settings: NodeSerializer) ‑> dict[str, typing.Any]

Serialize a graph into a dict .

Only nodes whose names appear in keys of settings are serialized into the result. Each NodeSerializer object can be built by factory methods on S.

GraphSpec().to_dict(
    graph,
    a = S.of(),
    b = S.name("B"),
)

Args

graph
A view of the graph.
_params_
Parameters passed to SerializationContext and used by serializers.
settings
NodeSerializer for each property.

Returns

Serialization result.

Expand source code
def to_dict(self, graph: GraphView, _params_: dict[str, dict[str, Any]] = {}, **settings: NodeSerializer) -> dict[str, Any]:
    """
    Serialize a graph into a `dict` .

    Only nodes whose names appear in keys of `settings` are serialized into the result.
    Each `NodeSerializer` object can be built by factory methods on `pyracmon.graph.serialize.S`.

    ```python
    GraphSpec().to_dict(
        graph,
        a = S.of(),
        b = S.name("B"),
    )
    ```

    Args:
        graph: A view of the graph.
        _params_: Parameters passed to `SerializationContext` and used by *serializer*s.
        settings: `NodeSerializer` for each property.
    Returns:
        Serialization result.
    """
    return SerializationContext(settings, self.find_serializers, _params_).execute(graph)
def to_schema(self, template: GraphTemplate, **settings: NodeSerializer) ‑> GraphSchema

Creates GraphSchema representing the structure of serialization result under given settings.

Args

template
Template of a graph.
settings
NodeSerializer for each property.

Returns

Schema of serialization result.

Expand source code
def to_schema(self, template: GraphTemplate, **settings: NodeSerializer) -> GraphSchema:
    """
    Creates `GraphSchema` representing the structure of serialization result under given settings.

    Args:
        template: Template of a graph.
        settings: `NodeSerializer` for each property.
    Returns:
        Schema of serialization result.
    """
    return GraphSchema(self, template, **settings)
class GraphTemplate (definitions: list[tuple[str, typing.Union[type[~T], typing_extensions.Self, GraphTemplate.Property], typing.Optional[IdentifyPolicy], typing.Optional[typing.Callable[[~T], bool]]]])

This class specifies the structure of a graph.

The template is composed of template properties each of which corresponds to a node container of a graph. Each template property can be obtained via an attribute of its name from the template.

Applying shift operator between properties creates the parent-child relationship between them. In next code, the template is composed of 4 properties where d is a child of c, and b and c are children of a.

template = GraphSpec().new_template(a=int, b=str, c=int, d=float)
template.a << [template.b, template.c]
template.c << template.d

Templates are merged when + is applied to them. The result has properties defined in both templates with keeping their relationships. Merging of templates having properties of the same name fails by raising ValueError.

Use GraphSpec.new_template() or other factory functions to create a template instead of using constructor directly.

Construct template with its properties. Don't use this constructor directly.

Args

definitions
Definitions of template properties.
Expand source code
class GraphTemplate:
    """
    This class specifies the structure of a graph.

    The template is composed of template properties each of which corresponds to a node container of a graph.
    Each template property can be obtained via an attribute of its name from the template.

    Applying shift operator between properties creates the parent-child relationship between them.
    In next code, the template is composed of 4 properties where `d` is a child of `c`, and `b` and `c` are children of `a`.

    ```python
    template = GraphSpec().new_template(a=int, b=str, c=int, d=float)
    template.a << [template.b, template.c]
    template.c << template.d
    ```

    Templates are merged when `+` is applied to them. The result has properties defined in both templates with keeping their relationships.
    Merging of templates having properties of the same name fails by raising `ValueError`.

    Use `GraphSpec.new_template` or other factory functions to create a template instead of using constructor directly.
    """
    class Property:
        """
        Template property which determines various behaviors of graph nodes.
        """
        def __init__(
            self,
            template: 'GraphTemplate',
            name: str,
            kind: Union[type[T], 'GraphTemplate'],
            policy: IdentifyPolicy,
            entity_filter: Optional[Callable[[T], bool]],
            origin: Optional[Self] = None,
        ):
            #: Graph template this property belongs to.
            self.template = template
            #: Property name.
            self.name = name
            #: Graph node bound to this property should have entity of this type.
            self.kind = kind
            #: Policy of entity identification.
            self.policy = policy
            #: Entity filter function.
            self.entity_filter = entity_filter
            self._origin = origin

        def _assert_canbe_parent(self, another):
            if another.parent is not None:
                raise ValueError(f"Graph template property can not have multiple parents.")
            if self.template != another.template:
                raise ValueError(f"Properties can make parent-child relationship only when they are declared in the same template.")
            if self == another:
                raise ValueError(f"Recursive relationship is not allowed.")
            if isinstance(self.kind, GraphTemplate):
                raise ValueError(f"Property for graph template can't have child.")
            p = self
            while p.parent is not None:
                if p.parent == another:
                    raise ValueError(f"Recursive relationship is not allowed.")
                p = p.parent

        @property
        def parents(self) -> Iterable['GraphTemplate.Property']:
            """
            Returns all parent properties.
            """
            return map(lambda r: r[1], filter(lambda r: r[0] == self, self.template._relations))

        @property
        def parent(self) -> Optional['GraphTemplate.Property']:
            """
            Returns parent property if exists.
            """
            return next(iter(self.parents), None)

        @property
        def children(self) -> list['GraphTemplate.Property']:
            """
            Returns child properties.
            """
            return [r[0] for r in self.template._relations if r[1] == self]

        @property
        def origin(self) -> 'GraphTemplate.Property':
            """
            Returns the original property in the property chain generated by adding templates.
            """
            p = self
            while p._origin:
                p = p._origin
            return p

        def is_compatible(self, other):
            return self.origin is other.origin

        def _move_template(self, dest: 'GraphTemplate', new_name=None) -> 'GraphTemplate.Property':
            """
            Copy this property and children with setting their templates to `dest` .
            """
            new_name = new_name or self.name
            prop = GraphTemplate.Property(dest, new_name, self.kind, self.policy, self.entity_filter, origin=self)
            _set_template_property(dest, prop)
            for c in self.children:
                cc = c._move_template(dest)
                prop << cc # pyright: ignore [reportUnusedExpression]
            return prop

        @overload
        def __lshift__(self, children: 'GraphTemplate.Property') -> 'GraphTemplate.Property': ...
        @overload
        def __lshift__(self, children: list['GraphTemplate.Property']) -> list['GraphTemplate.Property']: ...
        def __lshift__(self, children: Union['GraphTemplate.Property', list['GraphTemplate.Property']]) -> Union['GraphTemplate.Property', list['GraphTemplate.Property']]:
            """
            Makes this property as a parent of given properties.

            Args:
                children: Property or properties to be children of this property.
            Returns:
                The same object as the argument.
            """
            targets = [children] if isinstance(children, GraphTemplate.Property) else children
            for c in targets:
                self._assert_canbe_parent(c)
            self.template._relations += [(c, self) for c in targets]
            return children

        def __rshift__(self, parent: 'GraphTemplate.Property') -> 'GraphTemplate.Property':
            """
            Makes this property as a child of another property.

            Args:
                parent: A Property to be a parent of this property.
            Returns:
                The same object as the argument.
            """
            parent._assert_canbe_parent(self)
            self.template._relations += [(self, parent)]
            return parent

        def __rrshift__(self, children: Union['GraphTemplate.Property', list['GraphTemplate.Property']]) -> 'GraphTemplate.Property':
            """
            Reversed version of `__lshift__()` prepared to locate a list of properties on the left side.

            Args:
                children: Property or properties to be children of this property.
            Returns:
                The same object as the argument.
            """
            self.__lshift__(children)
            return self

    def __init__(self, definitions: list[tuple[
        str,
        Union[type[T], Self, Property],
        Optional[IdentifyPolicy],
        Optional[Callable[[T], bool]],
    ]]):
        """
        Construct template with its properties.  Don't use this constructor directly.

        Args:
            definitions: Definitions of template properties.
        """
        self._properties: dict[str, GraphTemplate.Property] = {}
        self._relations: list[tuple[GraphTemplate.Property, GraphTemplate.Property]] = []

        for d in definitions:
            name, kind, ident, ef = d

            ident = ident or neverPolicy()

            if isinstance(kind, GraphTemplate):
                prop = GraphTemplate.Property(self, name, kind, ident, None)
                _set_template_property(self, prop)
            elif isinstance(kind, GraphTemplate.Property):
                kind._move_template(self, name)
            else:
                _set_template_property(self, GraphTemplate.Property(self, name, kind, ident, ef))

    def __getattr__(self, key) -> 'GraphTemplate.Property':
        return self._properties[key]

    def __iter__(self) -> Iterator['GraphTemplate.Property']:
        """
        Iterates properties in parent-to-child order.

        Returns:
            Property iterator.
        """
        return _walk_properties(self._properties)

    def __iadd__(self, another: 'GraphTemplate') -> Self:
        """
        Adds another template to this template.

        Args:
            another: Another template.
        Returns:
            This instance.
        """
        for p in another._properties.values():
            prop = GraphTemplate.Property(self, p.name, p.kind, p.policy, p.entity_filter, origin=p)
            _set_template_property(self, prop)

        for n, p in another._relations:
            getattr(self, n.name) >> getattr(self, p.name) # pyright: ignore [reportUnusedExpression]

        return self

    def __add__(self, another: 'GraphTemplate') -> 'GraphTemplate':
        """
        Creates new template by merging this template and another one.

        Args:
            another: Another template.
        Returns:
            New template.
        """
        template = GraphTemplate([])
        template += self
        template += another

        return template

Class variables

var Property

Template property which determines various behaviors of graph nodes.

class IdentifyPolicy (identifier: Optional[Callable[[Any], Any]])

Provides entity identification functionalities used during appending entities to a graph.

Identification mechanism is based on the equality of identification keys extracted by entities.

Expand source code
class IdentifyPolicy:
    """
    Provides entity identification functionalities used during appending entities to a graph.

    Identification mechanism is based on the equality of identification keys extracted by entities.
    """
    def __init__(self, identifier: Optional[Callable[[Any], Any]]):
        #: A function to extract the identification key from an entity.
        self.identifier = identifier

    def get_identifier(self, value: Any) -> Any:
        """
        Returns identification key from an entity.

        Args:
            value: An entity.
        Returns:
            Identification key.
        """
        return self.identifier(value) if self.identifier else None

    def identify(
        self,
        prop: NodePropType,
        candidates: Iterable[MN],
        ancestors: Mapping[str, Iterable[MapNodeType[MapNodeType[MN, str], str]]],
    ) -> tuple[list[Optional[MN]], list[MN]]:
        """
        Select parent nodes and identical nodes of a new entity.

        This method is called during appending an entity to a graph.

        Args:
            prop: Template property for new entity.
            candidates: Nodes having the same identification key as the key of new entity.
            ancestors: Parent nodes mapped by property names.
        Returns
            The first item is a list of Parent nodes to which the node of new entity should be appended newly.
            `None` means to append a new node without parent. The second item is a list of identical nodes,
            which will be merged into ancestors and used in subsequent identifications of child entities.
        """
        raise NotImplementedError()

Subclasses

Instance variables

var identifier

A function to extract the identification key from an entity.

Methods

def get_identifier(self, value: Any) ‑> Any

Returns identification key from an entity.

Args

value
An entity.

Returns

Identification key.

Expand source code
def get_identifier(self, value: Any) -> Any:
    """
    Returns identification key from an entity.

    Args:
        value: An entity.
    Returns:
        Identification key.
    """
    return self.identifier(value) if self.identifier else None
def identify(self, prop: NodePropType, candidates: collections.abc.Iterable[+MN], ancestors: collections.abc.Mapping[str, collections.abc.Iterable[MapNodeType[MapNodeType[+MN, str], str]]]) ‑> tuple[list[typing.Optional[+MN]], list[+MN]]

Select parent nodes and identical nodes of a new entity.

This method is called during appending an entity to a graph.

Args

prop
Template property for new entity.
candidates
Nodes having the same identification key as the key of new entity.
ancestors
Parent nodes mapped by property names.

Returns The first item is a list of Parent nodes to which the node of new entity should be appended newly. None means to append a new node without parent. The second item is a list of identical nodes, which will be merged into ancestors and used in subsequent identifications of child entities.

Expand source code
def identify(
    self,
    prop: NodePropType,
    candidates: Iterable[MN],
    ancestors: Mapping[str, Iterable[MapNodeType[MapNodeType[MN, str], str]]],
) -> tuple[list[Optional[MN]], list[MN]]:
    """
    Select parent nodes and identical nodes of a new entity.

    This method is called during appending an entity to a graph.

    Args:
        prop: Template property for new entity.
        candidates: Nodes having the same identification key as the key of new entity.
        ancestors: Parent nodes mapped by property names.
    Returns
        The first item is a list of Parent nodes to which the node of new entity should be appended newly.
        `None` means to append a new node without parent. The second item is a list of identical nodes,
        which will be merged into ancestors and used in subsequent identifications of child entities.
    """
    raise NotImplementedError()
class S

An utility class to build NodeSerializer .

This class provides factory class methods to create NodeSerializer each of which works in the same way as the method of the same name declared on NodeSerializer .

Use them to supply NodeSerializers to functions to serialize a graph or to create a graph schema such as graph_dict or graph_schema .

graph_dict(
    graph,
    a = S.of(),
    b = S.head(),
)
Expand source code
class S(metaclass=SerializerMeta):
    """
    An utility class to build `NodeSerializer` .

    This class provides factory class methods to create `NodeSerializer`
    each of which works in the same way as the method of the same name declared on `NodeSerializer` .

    Use them to supply `NodeSerializer`s to functions to serialize a graph or to create a graph schema
    such as `graph_dict` or `graph_schema` .

    ```python
    graph_dict(
        graph,
        a = S.of(),
        b = S.head(),
    )
    ```
    """
    @classmethod
    def of(
        cls,
        namer: Optional[Union[str, Callable[[str], str]]] = None,
        aggregator: Optional[Union[Callable[[list[Node]], Node], Callable[[list[Node]], list[Node]]]] = None,
        *serializers: Serializer,
    ) -> 'NodeSerializer':
        """
        Create an instance of `NodeSerializer`.

        Args:
            namer: A string or naming function.
            aggregator: An aggregation function or an index of node to select in node container.
            serializer: A list of *serializer* s.
        Returns:
            Created `NodeSerializer` .
        """
        return NodeSerializer(namer, aggregator, *serializers)

Static methods

def of(namer: Union[str, Callable[[str], str], ForwardRef(None)] = None, aggregator: Union[Callable[[list[Node]], Node], Callable[[list[Node]], list[Node]], ForwardRef(None)] = None, *serializers: Callable[[ForwardRef('NodeContext')], Any]) ‑> NodeSerializer

Create an instance of NodeSerializer.

Args

namer
A string or naming function.
aggregator
An aggregation function or an index of node to select in node container.
serializer
A list of serializer s.

Returns

Created NodeSerializer .

Expand source code
@classmethod
def of(
    cls,
    namer: Optional[Union[str, Callable[[str], str]]] = None,
    aggregator: Optional[Union[Callable[[list[Node]], Node], Callable[[list[Node]], list[Node]]]] = None,
    *serializers: Serializer,
) -> 'NodeSerializer':
    """
    Create an instance of `NodeSerializer`.

    Args:
        namer: A string or naming function.
        aggregator: An aggregation function or an index of node to select in node container.
        serializer: A list of *serializer* s.
    Returns:
        Created `NodeSerializer` .
    """
    return NodeSerializer(namer, aggregator, *serializers)

Methods

def alter(*args, **kwargs)
Expand source code
def g(*args, **kwargs):
    ns = NodeSerializer()
    nf = getattr(ns, n)
    return nf(*args, **kwargs)
def at(*args, **kwargs)
Expand source code
def g(*args, **kwargs):
    ns = NodeSerializer()
    nf = getattr(ns, n)
    return nf(*args, **kwargs)
def doc(*args, **kwargs)
Expand source code
def g(*args, **kwargs):
    ns = NodeSerializer()
    nf = getattr(ns, n)
    return nf(*args, **kwargs)
def each(*args, **kwargs)
Expand source code
def g(*args, **kwargs):
    ns = NodeSerializer()
    nf = getattr(ns, n)
    return nf(*args, **kwargs)
def fold(*args, **kwargs)
Expand source code
def g(*args, **kwargs):
    ns = NodeSerializer()
    nf = getattr(ns, n)
    return nf(*args, **kwargs)
def head(*args, **kwargs)
Expand source code
def g(*args, **kwargs):
    ns = NodeSerializer()
    nf = getattr(ns, n)
    return nf(*args, **kwargs)
def last(*args, **kwargs)
Expand source code
def g(*args, **kwargs):
    ns = NodeSerializer()
    nf = getattr(ns, n)
    return nf(*args, **kwargs)
def merge(*args, **kwargs)
Expand source code
def g(*args, **kwargs):
    ns = NodeSerializer()
    nf = getattr(ns, n)
    return nf(*args, **kwargs)
def name(*args, **kwargs)
Expand source code
def g(*args, **kwargs):
    ns = NodeSerializer()
    nf = getattr(ns, n)
    return nf(*args, **kwargs)
def select(*args, **kwargs)
Expand source code
def g(*args, **kwargs):
    ns = NodeSerializer()
    nf = getattr(ns, n)
    return nf(*args, **kwargs)
def sub(*args, **kwargs)
Expand source code
def g(*args, **kwargs):
    ns = NodeSerializer()
    nf = getattr(ns, n)
    return nf(*args, **kwargs)