pyracmon.graph.spec API documentation
Description: This module provides a type which contains objects to control how graphs work.

# Module `pyracmon.graph.spec`

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

Expand source code

    """
    This module provides a type which contains objects to control how graphs work.
    """
    from typing import Callable, Any, Optional, TypeVar, Union
    from typing_extensions import Self
    from .identify import IdentifyPolicy, HierarchicalPolicy, neverPolicy
    from .template import GraphTemplate
    from .serialize import Serializer, SerializationContext, NodeSerializer
    from .schema import GraphSchema
    from .typing import issubtype
    from .graph import GraphView

    T = TypeVar('T')

    TypeDef = Union[type, GraphTemplate, GraphTemplate.Property]
    Identifier = Callable[[Any], Any]
    EntityFilter = Callable[[Any], bool]
    TemplateProperty = Union[
        type,
        tuple[()],
        tuple[type],
        tuple[type, Optional[Identifier]],
        tuple[type, Optional[Identifier], Optional[EntityFilter]],
    ]

    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)

## Classes

`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

  - ConfigurableSpec

  ### 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)