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.