pyracmon.graph.template API documentation
Description: This module provides a type specifying graph structure.

# Module `pyracmon.graph.template`

This module provides a type specifying graph structure.

Expand source code

    """
    This module provides a type specifying graph structure.
    """
    from typing import Any, Optional, Callable, TypeVar, Union, overload
    from typing_extensions import Self, dataclass_transform
    from collections.abc import Iterable, Iterator
    from pyracmon.graph.identify import IdentifyPolicy, neverPolicy

    T = TypeVar('T')

    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

    def _set_template_property(template: GraphTemplate, prop: GraphTemplate.Property):
        if prop.name in template._properties:
            raise ValueError(f"Property name '{prop.name}' conflicts.'")
        template._properties[prop.name] = prop

    def _walk_properties(properties: dict[str, GraphTemplate.Property], parent: Optional[GraphTemplate.Property] = None):
        def walk(p):
            yield p
            for q in p.children:
                for r in walk(q):
                    yield r

        for p in filter(lambda p: p.parent is parent, properties.values()):
            for q in walk(p):
                yield q

## Classes

`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.