pyracmon.graph.serialize API documentation

# Module `pyracmon.graph.serialize`

Expand source code

    from collections.abc import Iterator, Iterable
    from inspect import signature, Signature, getmembers, isfunction
    from typing import Any, Mapping, Optional, Union, Callable, Protocol, TypeVar, cast
    try:
        from typing import ParamSpec, TypeAlias
    except:
        from typing_extensions import ParamSpec, TypeAlias
    from .template import GraphTemplate
    from .graph import Node, NodeContainer, GraphView
    from .typing import Shrink, Extend, Typeable, issubgeneric, to_rawdict

    T = TypeVar('T')
    P = ParamSpec('P')

    # type aliases.
    Serializer: TypeAlias = Callable[['NodeContext'], Any]

    class NodeSerializing(Protocol):
        def doc(self, document: str, **options: Any) -> 'NodeSerializer': ...
        def name(self, name: str) -> 'NodeSerializer': ...
        def merge(self, namer: Optional[Callable[[str], str]] = None) -> 'NodeSerializer': ...
        def at(self, index: int, alt: Any = None) -> 'NodeSerializer': ...
        def head(self, alt: Any = None) -> 'NodeSerializer': ...
        def last(self, alt: Any = None) -> 'NodeSerializer': ...
        def fold(self, aggregator: Callable[[list[Node]], Any]) -> 'NodeSerializer': ...
        def select(self, aggregator: Callable[[list[Node]], list[Node]]) -> 'NodeSerializer': ...
        def each(self, func: Serializer) -> 'NodeSerializer': ...
        def sub(self, **settings) -> 'NodeSerializer': ...
        def alter(
            self,
            generator: Optional[Serializer] = None,
            excludes: Optional[Iterable[str]] = None,
            includes: Optional[Iterable[str]] = None,
        ) -> 'NodeSerializer': ...

    class NodeSerializer(NodeSerializing):
        """
        This class provides ways to configure serialization result for a node container.

        Graph is serialized into `dict` from root node containers to their descendants.
        `NodeSerializer` should be set to each container (= template property) to control how to serialize nodes in it.

        At first, nodes to be serialized are selected by a node container using *aggregator*
        which is a function or sequence of functions extracting a node or nodes from a node container.
        `fold` and `select` are the general methods to set *aggregator* to `NodeSerializer` .

        ```python
        >>> # NodeSerializer to select first node in the container.
        >>> S.fold(lambda ns: ns[0])
        >>> # NodeSerializer to select every other node.
        >>> S.fold(lambda ns: ns[0::2])
        ```

        Each selected node is serialized in the way determined by the type of corresponding template property.
        Serialization function (= *serializer* ) is obtained usually from `pyracmon.graph.spec.GraphSpec`
        where *serializer* are stored with being related with applicable types respectively.
        Additionally, *serializer*s can be set to `NodeSerializer` directly by `each` or some other methods.
        For each node, all valid *serializer*s are collected and merged into a function which finally is applied to its entity.

        ```python
        >>> spec = GraphSpec()
        >>> # Register a serializer for int type which converts an int into a dict.
        >>> spec.add_serializer(int, lambda v: dict(v=v))
        >>> # Set serializer which multiplies values in dict.
        >>> ns = S.each(lambda cxt: {k:v*2 for k,v in cxt.serialize()})
        >>> # Do serialization
        >>> graph.append(a=1).append(a=2)
        >>> spec.to_dict(
        >>>     graph.view,
        >>>     a = ns,
        >>> )
        {"a": [{"v": 2}, {"v": 4}]}
        ```

        Only when a node is serialized into `dict` ,
        its child nodes are serialized succeedingly and the result is put into the `dict` with the same keys as their property names.
        The key can be changed by set *namer* to the `NodeSerializer` by `name` .

        Here, `merge` is a special configuration of `NodeSerializer` ,
        which can be used for the case that a child node is also serialized into `dict` and it is wanted to be merged into parent `dict` .
        It can take a callable which converts key in original child `dict` into another key used in parent `dict` .

        Whether the child should be put or merge into parent `dict` is determined whether *namer* is `str` (or `None`) or `Callable` .

        ```python
        >>> graph.append(a=dict(a1=1, a2=2), b=dict(b1=3, b2=4))
        >>> GraphSpec().to_dict(
        >>>     graph.view,
        >>>     a = S.of(),
        >>>     b = S.merge(lambda n: f"__{n}__"),
        >>> )
        {"a": [{"a1":1, "a2":2, "__b1__":3, "__b2__":4}]}
        ```

        Args:
            namer: A string or a function determining the key in parent `dict` .
            aggregator: A function to select node(s) from the node container.
            serializers: List of *serializer* s.
        """
        def __init__(
            self,
            namer: Optional[Union[str, Callable[[str], str]]] = None,
            aggregator: Optional[Union[Callable[[list[Node]], Node], Callable[[list[Node]], list[Node]]]] = None,
            *serializers: Serializer,
        ):
            self._namer = namer
            self._aggregator = aggregator
            self._serializers = list(serializers)
            self._be_merged = False
            self._doc = ""
            self._doc_options = {}

        @property
        def namer(self) -> Callable[[str], str]:
            """
            Returns *namer* in the form of function even when not to merge.
            """
            def f(v: str) -> str:
                if self._namer is None:
                    return v
                elif isinstance(self._namer, str):
                    return self._namer
                else:
                    return self._namer(v)
            return f

        @property
        def aggregator(self) -> Callable[[list[Node]], Union[list[Node], Node, Any]]:
            """
            Returns *aggregator* supplied with correct return annotation.
            """
            if self._aggregator is None:
                def agg1(values: list[T]) -> list[T]:
                    return values
                return agg1
            elif signature(self._aggregator).return_annotation == Signature.empty:
                # TODO: No return annotation implies list to list aggregation.
                def agg2(values: list[T]) -> list[T]:
                    return self._aggregator(values) # type: ignore
                return agg2
            else:
                return self._aggregator

        @property
        def serializer(self) -> Serializer:
            """
            Returns merged *serializer* which has correctly annotated signature.
            """
            return chain_serializers(self._serializers)

        @property
        def be_merged(self) -> bool:
            """
            Returns whether the converted value will be merged into parent.
            """
            return self._be_merged

        @property
        def be_singular(self) -> bool:
            """
            Returns whether the converted value will be a singular object, not a list.

            This is estimated by annotation of aggregation function. If its returning type is not annotated, this property always returns `False` .
            Builder methods adds appropriate annotation to given function when it does not have the annotation.
            """
            rt = signature(self.aggregator).return_annotation
            return not issubgeneric(rt, list)

        def _set_aggregator(self, aggregator, folds):
            try:
                rt = signature(aggregator).return_annotation
            except:
                rt = Signature.empty

            if rt == Signature.empty:
                def agg(vs: list[T]) -> (T if folds else list[T]):
                    return aggregator(vs)
                self._aggregator = agg
            elif issubgeneric(rt, list) ^ (not folds):
                raise ValueError(f"Return annotation of function is not valid.")
            else:
                self._aggregator = aggregator

            return self

        #----------------------------------------------------------------
        # Documentation
        #----------------------------------------------------------------
        def doc(self, document: str, **options: Any) -> 'NodeSerializer':
            """
            Set the documentation for this node.

            `document` is used in graph schema as a parameter of `Annotated` .

            Args:
                document: A documentation string.
                options: Documentation options.
            Returns:
                This instance.
            """
            self._doc = document
            self._doc_options = options
            return self

        #----------------------------------------------------------------
        # Naming
        #----------------------------------------------------------------
        def name(self, name: str) -> 'NodeSerializer':
            """
            Set a key in parent dictionary.

            Args:
                name: A key string.
            Returns:
                This instance.
            """
            if not isinstance(name, str):
                raise ValueError(f"The name of node must be a string but {type(name)} is given.")
            self._namer = name
            self._be_merged = False
            return self

        def merge(self, namer: Optional[Callable[[str], str]] = None) -> 'NodeSerializer':
            """
            Set a naming function for merging into parent.

            Because merging needs folding, this method overrides the aggregation function by invoking `head()` internally
            if this instance is not configured to fold nodes into a single node.

            Args:
                namer: The naming function. If `None`, the property is used as it is.
            Returns:
                This instance.
            """
            if namer and not callable(namer):
                raise ValueError(f"The method merging a node into its parent node must be callable or None.")
            self._namer = namer or (lambda x:x)
            self._be_merged = True
            if not self.be_singular:
                self.head()
            return self

        #----------------------------------------------------------------
        # Aggregation
        #----------------------------------------------------------------
        def at(self, index: int, alt: Any = None) -> 'NodeSerializer':
            """
            Set an *aggregator* which picks up the node at the index.

            Args:
                index: An index of the element.
                alt: A value used when no node is found at the index.
            Returns:
                This instance.
            """
            def agg(vs: list[T]) -> (Optional[T] if alt is None else T):
                return vs[index] if len(vs) > index else alt
            #return self.fold(lambda vs: vs[index] if len(vs) > index else alt)
            return self.fold(agg)

        def head(self, alt: Any = None) -> 'NodeSerializer':
            """
            Set an *aggregator* which picks up the first node.

            Args:
                alt: A value used when there exists no nodes.
            Returns:
                This instance.
            """
            return self.at(0, alt)

        def last(self, alt: Any = None) -> 'NodeSerializer':
            """
            Set an *aggregator* which picks up the last node.

            Args:
                alt: A value used when there exists no nodes.
            Returns:
                This instance.
            """
            def agg(vs: list[T]) -> (Optional[T] if alt is None else T):
                return vs[-1] if len(vs) > 0 else alt
            #return self.fold(lambda vs: vs[-1] if len(vs) > 0 else alt)
            return self.fold(agg)

        def fold(self, aggregator: Callable[[list[Node]], Any]) -> 'NodeSerializer':
            """
            Set an aggregation function converting a list of nodes into a single node or any value.

            Args:
                aggregator: An aggregation function.
            Returns:
                This instance.
            """
            return self._set_aggregator(aggregator, True)

        def select(self, aggregator: Callable[[list[Node]], list[Node]]) -> 'NodeSerializer':
            """
            Set an aggregation function selecting a list of nodes from all nodes from the container.

            Args:
                aggregator: An aggregation function.
            Returns:
                This instance.
            """
            return self._set_aggregator(aggregator, False)

        #----------------------------------------------------------------
        # Serizlization
        #----------------------------------------------------------------
        def each(self, func: Serializer) -> 'NodeSerializer':
            """
            Set a *serializer* .

            *serializer* is a function which will be invoked with a single argument of `NodeContext` ,
            from which internal code of *serializer* can get information of the node.

            For the sake of static typing, the *serializer* should have correct returns annotation.

            Args:
                func: A function converting a node entity into a value.
            Returns:
                This instance.
            """
            self._serializers.append(func)
            return self

        def sub(self, **settings) -> 'NodeSerializer':
            """
            Set serialization settings for sub graph.

            This method is used for the property whose kind is `GraphTemplate` .
            The form of `settings` is same as keyword arguments used to serialize the graph.

            Args:
                settings: Serialization settings used to serialize sub graph.
            Returns:
                This instance.
            """
            from pyracmon.graph.schema import GraphSchema
            class SubGraph(Typeable[T]):
                serializers = settings.copy()

                @staticmethod
                def resolve(sub_graph, bound, arg, spec):
                    return GraphSchema(spec, arg.template, **sub_graph.serializers).schema

            def to_dict(cxt: NodeContext) -> SubGraph[T]:
                vv = cxt.serialize()
                return SerializationContext(
                    settings,
                    cxt.context.finder if cxt.context else lambda t: [],
                ).execute(vv.view) # type: ignore

            self._serializers.append(to_dict)
            return self.head()

        def alter(
            self,
            generator: Optional[Serializer] = None,
            excludes: Optional[Iterable[str]] = None,
            includes: Optional[Iterable[str]] = None,
        ) -> 'NodeSerializer':
            """
            Extends and shrinks the dictionary obtained as a result of *serializer*s applied beforehand.

            `generator` is a kind of *serializer* returning `dict` which will be merged into serialization result.
            This can be used to add extra key value pairs into the result.
            For the sake of static typing, `generator` should have correct return annotation of `TypedDict` .

            `excludes` and `includes` are used to select keys from the result.

            Args:
                generator: A function generating dictionary to be merged.
                excludes: Keys to exclude.
                includes: Keys to keep.
            Returns:
                This instance.
            """
            excludes = excludes or []

            class EachExtend(Extend[T]):
                @classmethod
                def schema(cls, bound, arg):
                    return signature(generator).return_annotation if generator else Signature.empty

            class EachShrink(Shrink[T]):
                @classmethod
                def select(cls, td, bound):
                    return excludes, includes

            def convert(cxt) -> EachShrink[EachExtend[T]]:
                ext = generator(cxt) if generator else {}
                vv = cxt.serialize()
                vv.update(**to_rawdict(ext, True))
                return {k:v for k, v in vv.items() if (not includes or k in includes) and k not in excludes} # type: ignore

            self._serializers.append(convert)
            return self

    class NodeParams:
        def __init__(self, params) -> None:
            self._params: dict[str, Any] = params

        def __getattr__(self, key) -> Optional[Any]:
            """
            Returns a value by key from values passed from invoking scope being bound for the node.
            """
            return self._params.get(key, None)

    class NodeContext:
        """
        A class containing informations for serialization of a single node.

        The instance of this class is passed to the serialization function.
        Properties listed below are available to control serialization.

        - context: `SerializationContext` for the serialization of the graph.
        - node: `Node` to serialize.
        - value: Entity value of the `Node` .
        - params: Arbitrary values which is passed from invoking scope with being bound to the key of node name.

        Every serializer has to call `serialize()` to get the result of preceeding serializers,
        or make a result direcly from the node.
        """
        def __init__(self, context: 'SerializationContext', params: NodeParams) -> None:
            #: `SerializationContext` for the serializaion of the graph.`
            self.context = context
            #: Arbitrary values passed by outside for the node.
            self.params = params
            # Set on demand.
            self._node: Optional[Node] = None
            self._iterator: Optional[Iterator[Any]] = None

        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc_value, traceback):
            self._node = None
            self._iterator = None

        @property
        def node(self) -> Node:
            # Node must be set when passed to serialization function.
            return cast(Node, self._node)

        @property
        def value(self) -> Any:
            return cast(Node, self._node).entity

        def serialize(self) -> Any:
            """
            Obtain a value serialized by preceeding serializers.
            """
            try:
                return next(cast(Iterator[Any], self._iterator))(self)
            except StopIteration:
                return self.node.entity

    class NodeContextFactory:
        """
        This class generates a `NodeContext` for nodes bound to a template property.

        Args:
            context: `SerializationContext` for the serialization of a graph.
            serializers: Globally registered serializers for the type of node entity.
            params: Parameters given at the serialization by caller.
        """
        def __init__(
            self,
            context: 'SerializationContext',
            serializers: list[Serializer],
            params: dict[str, Any],
        ) -> None:
            self.serializers = serializers
            # Generate and keep an instance of NodeContext to save memory for big graph.
            self.node_context = NodeContext(context, NodeParams(params))

        def begin(self, node, serializers) -> NodeContext:
            self.node_context._node = node
            self.node_context._iterator = iter((self.serializers + serializers)[::-1])
            return self.node_context

    class SerializationContext:
        """
        This class implements actual serialization flow applied to a graph.

        `node_params` is a `dict` whose values will be passed to *serializer* via `params` attribute of `NodeContext` .
        The property name of the node is used to get values (also a `dict` ) from the `dict`
        and the `params` exposes them as its attributes of the same names as their keys.

        Arbitrary values can be passed in `node_params` which is a `dict`

        Following code shows the example passing a parameter to a *serializer* .

        ```python
        cxt = SerializationContext(
            dict(
                a = S.each(lambda cxt: cxt.value*c.params.value),
            ),
            finder,
            dict(a={"value": 10})
        )
        ```

        Args:
            settings: Mapping of node name to `NodeSerializer` .
            finder: A function to find base *serializer* s by a `type` .
            node_params: Arbitrary parameters passed to *serializer*s.
        """
        def __init__(
            self,
            settings: dict[str, NodeSerializer],
            finder: Callable[[type], list[Serializer]],
            node_params: Optional[dict[str, dict[str, Any]]] = None,
        ):
            self.settings: dict[str, NodeSerializer] = settings
            self.finder: Callable[[type], list[Serializer]] = finder
            self._node_params: dict[str, dict[str, Any]] = node_params or {}
            self._context_factories: dict[str, NodeContextFactory] = {}

        def __getitem__(self, node: Union[Node, str]) -> Any:
            """
            Returns an accessor to parameters for given node.

            Args:
                node: Node or node name.
            Returns:
                An object exposing parameters for the node as attributes.
            """
            name = node.name if isinstance(node, Node) else node
            params = self._node_params.get(name, {})

            class Accessor:
                def __getattr__(self, key):
                    return params.get(key, None)

            return Accessor()

        def execute(self, graph: GraphView) -> dict[str, Any]:
            """
            Serializes a graph.

            Args:
                graph: The view of graph to serialize.
            Returns:
                Serialization result.
            """
            result = {}
            for c in graph().roots:
                self.serialize_to(c.name, c, result)
            return result

        def serialize_to(self, name: str, container: Union[NodeContainer, Node.Children], parent: dict[str, Any]) -> None:
            """
            Serialize nodes and appends them into the dictionary.

            Args:
                name: Name of the template property associated with the nodes.
                container: Container of nodes.
                parent: A parent dictionary to which serialized values will be appended.
            """
            ns = self.settings.get(name, None)

            if not ns:
                # Nodes whose names are not supplied to settings are not serialized.
                return

            # First, aggregate nodes into its subset or a single node.
            nodes: Union[list[Node], Node, Any] = ns.aggregator(container.nodes)

            if ns.be_singular:
                if isinstance(nodes, list):
                    raise ValueError(f"Aggregation function is marked to create a single value but returns node list.")

                # Alternative value given to aggregation function may be returned instead of node.
                value = self._serialize_node(nodes, ns) if isinstance(nodes, Node) else nodes

                if ns.be_merged:
                    if value is None:
                        # When empty, no key-value pair is added to parent.
                        return
                    elif not isinstance(value, dict):
                        raise ValueError(f"Serialized value must be dict but {type(value)}.")

                    parent.update({ns.namer(k):v for k, v in value.items()})
                else:
                    parent[ns.namer(name)] = value
            else:
                if not isinstance(nodes, list):
                    raise ValueError(f"Aggregation function is marked to return node list but returns a single value.")
                if ns.be_merged:
                    raise ValueError(f"Merging to parent dict requires folding.")

                parent[ns.namer(name)] = [self._serialize_node(n, ns) for n in nodes]

        def _find_serializer(self, prop: GraphTemplate.Property) -> list[Serializer]:
            return self.finder(prop.kind) if isinstance(prop.kind, type) else []

        def _serialize_node(self, node: Node, node_serializer: NodeSerializer):
            if not node.prop.name in self._context_factories:
                self._context_factories[node.prop.name] = NodeContextFactory(
                    self,
                    self._find_serializer(node.prop),
                    self._node_params.get(node.prop.name, {}),
                )

            factory = self._context_factories[node.prop.name]

            with factory.begin(node, node_serializer._serializers) as cxt:
                value = cxt.serialize()

                # Child nodes are serialized only when the parent node is serialized into a dict.
                if isinstance(value, dict):
                    for n, ch in node.children.items():
                        self.serialize_to(n, ch, value)

                return value

    class SerializerMeta(NodeSerializing, type): # type: ignore
        @classmethod
        def __prepare__(cls, __name: str, __bases: tuple[type, ...], **kwds: Any) -> Mapping[str, object]:
            def wrap(n, f):
                def g(*args, **kwargs):
                    ns = NodeSerializer()
                    nf = getattr(ns, n)
                    return nf(*args, **kwargs)
                return g
            return {n: wrap(n, f) for n, f in getmembers(NodeSerializing, isfunction) if not n.startswith("__")}

    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)

    def chain_serializers(serializers: list[Serializer]) -> Serializer:
        """
        Creates a serializer which chains given serializers.

        Args:
            serializers: A list of serializers.
        Returns:
            Chained serializer.
        """
        def merge(fs) -> type:
            rt = Signature.empty
            for f in fs[::-1]:
                t = signature(f).return_annotation
                if t != Signature.empty:
                    try:
                        t[T]
                        rt = t if rt == Signature.empty else rt[t] # type: ignore
                    except TypeError:
                        try:
                            return rt[t] # type: ignore
                        except TypeError:
                            return t
            return rt

        rt = merge(serializers)
        def composed(cxt) -> rt: # type: ignore
            cxt._iterator = iter(serializers[::-1] + list(cxt._iterator))
            return cxt.serialize()

        return composed

## Functions

`def chain_serializers(serializers: list[typing.Callable[[ForwardRef('NodeContext')], typing.Any]]) ‑> Callable[[NodeContext], Any]`

  Creates a serializer which chains given serializers.

  ## Args

  `serializers`
    A list of serializers.

  ## Returns

  Chained serializer.

  Expand source code

      def chain_serializers(serializers: list[Serializer]) -> Serializer:
          """
          Creates a serializer which chains given serializers.

          Args:
              serializers: A list of serializers.
          Returns:
              Chained serializer.
          """
          def merge(fs) -> type:
              rt = Signature.empty
              for f in fs[::-1]:
                  t = signature(f).return_annotation
                  if t != Signature.empty:
                      try:
                          t[T]
                          rt = t if rt == Signature.empty else rt[t] # type: ignore
                      except TypeError:
                          try:
                              return rt[t] # type: ignore
                          except TypeError:
                              return t
              return rt

          rt = merge(serializers)
          def composed(cxt) -> rt: # type: ignore
              cxt._iterator = iter(serializers[::-1] + list(cxt._iterator))
              return cxt.serialize()

          return composed

## Classes

`class NodeContext (context: SerializationContext, params: NodeParams)`

  A class containing informations for serialization of a single node.

  The instance of this class is passed to the serialization function. Properties listed below are available to control serialization.

  - context: `SerializationContext` for the serialization of the graph.
  - node: `Node` to serialize.
  - value: Entity value of the `Node` .
  - params: Arbitrary values which is passed from invoking scope with being bound to the key of node name.

  Every serializer has to call `serialize()` to get the result of preceeding serializers, or make a result direcly from the node.

  Expand source code

      class NodeContext:
          """
          A class containing informations for serialization of a single node.

          The instance of this class is passed to the serialization function.
          Properties listed below are available to control serialization.

          - context: `SerializationContext` for the serialization of the graph.
          - node: `Node` to serialize.
          - value: Entity value of the `Node` .
          - params: Arbitrary values which is passed from invoking scope with being bound to the key of node name.

          Every serializer has to call `serialize()` to get the result of preceeding serializers,
          or make a result direcly from the node.
          """
          def __init__(self, context: 'SerializationContext', params: NodeParams) -> None:
              #: `SerializationContext` for the serializaion of the graph.`
              self.context = context
              #: Arbitrary values passed by outside for the node.
              self.params = params
              # Set on demand.
              self._node: Optional[Node] = None
              self._iterator: Optional[Iterator[Any]] = None

          def __enter__(self):
              return self

          def __exit__(self, exc_type, exc_value, traceback):
              self._node = None
              self._iterator = None

          @property
          def node(self) -> Node:
              # Node must be set when passed to serialization function.
              return cast(Node, self._node)

          @property
          def value(self) -> Any:
              return cast(Node, self._node).entity

          def serialize(self) -> Any:
              """
              Obtain a value serialized by preceeding serializers.
              """
              try:
                  return next(cast(Iterator[Any], self._iterator))(self)
              except StopIteration:
                  return self.node.entity

  ### Instance variables

  `var context`

    `SerializationContext` for the serializaion of the graph.`

  `var node : Node`
    Expand source code

        @property
        def node(self) -> Node:
            # Node must be set when passed to serialization function.
            return cast(Node, self._node)

  `var params`

    Arbitrary values passed by outside for the node.

  `var value : Any`
    Expand source code

        @property
        def value(self) -> Any:
            return cast(Node, self._node).entity

  ### Methods

  `def serialize(self) ‑> Any`

    Obtain a value serialized by preceeding serializers.

    Expand source code

        def serialize(self) -> Any:
            """
            Obtain a value serialized by preceeding serializers.
            """
            try:
                return next(cast(Iterator[Any], self._iterator))(self)
            except StopIteration:
                return self.node.entity

`class NodeContextFactory (context: SerializationContext, serializers: list[typing.Callable[[ForwardRef('NodeContext')], typing.Any]], params: dict[str, typing.Any])`

  This class generates a `NodeContext` for nodes bound to a template property.

  ## Args

  `context`
    `SerializationContext` for the serialization of a graph.
  `serializers`
    Globally registered serializers for the type of node entity.
  `params`
    Parameters given at the serialization by caller.
  Expand source code

      class NodeContextFactory:
          """
          This class generates a `NodeContext` for nodes bound to a template property.

          Args:
              context: `SerializationContext` for the serialization of a graph.
              serializers: Globally registered serializers for the type of node entity.
              params: Parameters given at the serialization by caller.
          """
          def __init__(
              self,
              context: 'SerializationContext',
              serializers: list[Serializer],
              params: dict[str, Any],
          ) -> None:
              self.serializers = serializers
              # Generate and keep an instance of NodeContext to save memory for big graph.
              self.node_context = NodeContext(context, NodeParams(params))

          def begin(self, node, serializers) -> NodeContext:
              self.node_context._node = node
              self.node_context._iterator = iter((self.serializers + serializers)[::-1])
              return self.node_context

  ### Methods

  `def begin(self, node, serializers) ‑> NodeContext`
    Expand source code

        def begin(self, node, serializers) -> NodeContext:
            self.node_context._node = node
            self.node_context._iterator = iter((self.serializers + serializers)[::-1])
            return self.node_context

`class NodeParams (params)`
  Expand source code

      class NodeParams:
          def __init__(self, params) -> None:
              self._params: dict[str, Any] = params

          def __getattr__(self, key) -> Optional[Any]:
              """
              Returns a value by key from values passed from invoking scope being bound for the node.
              """
              return self._params.get(key, None)

`class NodeSerializer (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])`

  This class provides ways to configure serialization result for a node container.

  Graph is serialized into `dict` from root node containers to their descendants. `NodeSerializer` should be set to each container (= template property) to control how to serialize nodes in it.

  At first, nodes to be serialized are selected by a node container using aggregator which is a function or sequence of functions extracting a node or nodes from a node container. `fold` and `select` are the general methods to set aggregator to `NodeSerializer` .

      >>> # NodeSerializer to select first node in the container.
      >>> S.fold(lambda ns: ns[0])
      >>> # NodeSerializer to select every other node.
      >>> S.fold(lambda ns: ns[0::2])

  Each selected node is serialized in the way determined by the type of corresponding template property. Serialization function (= serializer ) is obtained usually from `GraphSpec` where serializer are stored with being related with applicable types respectively. Additionally, serializers can be set to `NodeSerializer` directly by `each` or some other methods. For each node, all valid serializers are collected and merged into a function which finally is applied to its entity.

      >>> spec = GraphSpec()
      >>> # Register a serializer for int type which converts an int into a dict.
      >>> spec.add_serializer(int, lambda v: dict(v=v))
      >>> # Set serializer which multiplies values in dict.
      >>> ns = S.each(lambda cxt: {k:v*2 for k,v in cxt.serialize()})
      >>> # Do serialization
      >>> graph.append(a=1).append(a=2)
      >>> spec.to_dict(
      >>>     graph.view,
      >>>     a = ns,
      >>> )
      {"a": [{"v": 2}, {"v": 4}]}

  Only when a node is serialized into `dict` , its child nodes are serialized succeedingly and the result is put into the `dict` with the same keys as their property names. The key can be changed by set namer to the `NodeSerializer` by `name` .

  Here, `merge` is a special configuration of `NodeSerializer` , which can be used for the case that a child node is also serialized into `dict` and it is wanted to be merged into parent `dict` . It can take a callable which converts key in original child `dict` into another key used in parent `dict` .

  Whether the child should be put or merge into parent `dict` is determined whether namer is `str` (or `None`) or `Callable` .

      >>> graph.append(a=dict(a1=1, a2=2), b=dict(b1=3, b2=4))
      >>> GraphSpec().to_dict(
      >>>     graph.view,
      >>>     a = S.of(),
      >>>     b = S.merge(lambda n: f"__{n}__"),
      >>> )
      {"a": [{"a1":1, "a2":2, "__b1__":3, "__b2__":4}]}

  ## Args

  `namer`
    A string or a function determining the key in parent `dict` .
  `aggregator`
    A function to select node(s) from the node container.
  `serializers`
    List of serializer s.
  Expand source code

      class NodeSerializer(NodeSerializing):
          """
          This class provides ways to configure serialization result for a node container.

          Graph is serialized into `dict` from root node containers to their descendants.
          `NodeSerializer` should be set to each container (= template property) to control how to serialize nodes in it.

          At first, nodes to be serialized are selected by a node container using *aggregator*
          which is a function or sequence of functions extracting a node or nodes from a node container.
          `fold` and `select` are the general methods to set *aggregator* to `NodeSerializer` .

          ```python
          >>> # NodeSerializer to select first node in the container.
          >>> S.fold(lambda ns: ns[0])
          >>> # NodeSerializer to select every other node.
          >>> S.fold(lambda ns: ns[0::2])
          ```

          Each selected node is serialized in the way determined by the type of corresponding template property.
          Serialization function (= *serializer* ) is obtained usually from `pyracmon.graph.spec.GraphSpec`
          where *serializer* are stored with being related with applicable types respectively.
          Additionally, *serializer*s can be set to `NodeSerializer` directly by `each` or some other methods.
          For each node, all valid *serializer*s are collected and merged into a function which finally is applied to its entity.

          ```python
          >>> spec = GraphSpec()
          >>> # Register a serializer for int type which converts an int into a dict.
          >>> spec.add_serializer(int, lambda v: dict(v=v))
          >>> # Set serializer which multiplies values in dict.
          >>> ns = S.each(lambda cxt: {k:v*2 for k,v in cxt.serialize()})
          >>> # Do serialization
          >>> graph.append(a=1).append(a=2)
          >>> spec.to_dict(
          >>>     graph.view,
          >>>     a = ns,
          >>> )
          {"a": [{"v": 2}, {"v": 4}]}
          ```

          Only when a node is serialized into `dict` ,
          its child nodes are serialized succeedingly and the result is put into the `dict` with the same keys as their property names.
          The key can be changed by set *namer* to the `NodeSerializer` by `name` .

          Here, `merge` is a special configuration of `NodeSerializer` ,
          which can be used for the case that a child node is also serialized into `dict` and it is wanted to be merged into parent `dict` .
          It can take a callable which converts key in original child `dict` into another key used in parent `dict` .

          Whether the child should be put or merge into parent `dict` is determined whether *namer* is `str` (or `None`) or `Callable` .

          ```python
          >>> graph.append(a=dict(a1=1, a2=2), b=dict(b1=3, b2=4))
          >>> GraphSpec().to_dict(
          >>>     graph.view,
          >>>     a = S.of(),
          >>>     b = S.merge(lambda n: f"__{n}__"),
          >>> )
          {"a": [{"a1":1, "a2":2, "__b1__":3, "__b2__":4}]}
          ```

          Args:
              namer: A string or a function determining the key in parent `dict` .
              aggregator: A function to select node(s) from the node container.
              serializers: List of *serializer* s.
          """
          def __init__(
              self,
              namer: Optional[Union[str, Callable[[str], str]]] = None,
              aggregator: Optional[Union[Callable[[list[Node]], Node], Callable[[list[Node]], list[Node]]]] = None,
              *serializers: Serializer,
          ):
              self._namer = namer
              self._aggregator = aggregator
              self._serializers = list(serializers)
              self._be_merged = False
              self._doc = ""
              self._doc_options = {}

          @property
          def namer(self) -> Callable[[str], str]:
              """
              Returns *namer* in the form of function even when not to merge.
              """
              def f(v: str) -> str:
                  if self._namer is None:
                      return v
                  elif isinstance(self._namer, str):
                      return self._namer
                  else:
                      return self._namer(v)
              return f

          @property
          def aggregator(self) -> Callable[[list[Node]], Union[list[Node], Node, Any]]:
              """
              Returns *aggregator* supplied with correct return annotation.
              """
              if self._aggregator is None:
                  def agg1(values: list[T]) -> list[T]:
                      return values
                  return agg1
              elif signature(self._aggregator).return_annotation == Signature.empty:
                  # TODO: No return annotation implies list to list aggregation.
                  def agg2(values: list[T]) -> list[T]:
                      return self._aggregator(values) # type: ignore
                  return agg2
              else:
                  return self._aggregator

          @property
          def serializer(self) -> Serializer:
              """
              Returns merged *serializer* which has correctly annotated signature.
              """
              return chain_serializers(self._serializers)

          @property
          def be_merged(self) -> bool:
              """
              Returns whether the converted value will be merged into parent.
              """
              return self._be_merged

          @property
          def be_singular(self) -> bool:
              """
              Returns whether the converted value will be a singular object, not a list.

              This is estimated by annotation of aggregation function. If its returning type is not annotated, this property always returns `False` .
              Builder methods adds appropriate annotation to given function when it does not have the annotation.
              """
              rt = signature(self.aggregator).return_annotation
              return not issubgeneric(rt, list)

          def _set_aggregator(self, aggregator, folds):
              try:
                  rt = signature(aggregator).return_annotation
              except:
                  rt = Signature.empty

              if rt == Signature.empty:
                  def agg(vs: list[T]) -> (T if folds else list[T]):
                      return aggregator(vs)
                  self._aggregator = agg
              elif issubgeneric(rt, list) ^ (not folds):
                  raise ValueError(f"Return annotation of function is not valid.")
              else:
                  self._aggregator = aggregator

              return self

          #----------------------------------------------------------------
          # Documentation
          #----------------------------------------------------------------
          def doc(self, document: str, **options: Any) -> 'NodeSerializer':
              """
              Set the documentation for this node.

              `document` is used in graph schema as a parameter of `Annotated` .

              Args:
                  document: A documentation string.
                  options: Documentation options.
              Returns:
                  This instance.
              """
              self._doc = document
              self._doc_options = options
              return self

          #----------------------------------------------------------------
          # Naming
          #----------------------------------------------------------------
          def name(self, name: str) -> 'NodeSerializer':
              """
              Set a key in parent dictionary.

              Args:
                  name: A key string.
              Returns:
                  This instance.
              """
              if not isinstance(name, str):
                  raise ValueError(f"The name of node must be a string but {type(name)} is given.")
              self._namer = name
              self._be_merged = False
              return self

          def merge(self, namer: Optional[Callable[[str], str]] = None) -> 'NodeSerializer':
              """
              Set a naming function for merging into parent.

              Because merging needs folding, this method overrides the aggregation function by invoking `head()` internally
              if this instance is not configured to fold nodes into a single node.

              Args:
                  namer: The naming function. If `None`, the property is used as it is.
              Returns:
                  This instance.
              """
              if namer and not callable(namer):
                  raise ValueError(f"The method merging a node into its parent node must be callable or None.")
              self._namer = namer or (lambda x:x)
              self._be_merged = True
              if not self.be_singular:
                  self.head()
              return self

          #----------------------------------------------------------------
          # Aggregation
          #----------------------------------------------------------------
          def at(self, index: int, alt: Any = None) -> 'NodeSerializer':
              """
              Set an *aggregator* which picks up the node at the index.

              Args:
                  index: An index of the element.
                  alt: A value used when no node is found at the index.
              Returns:
                  This instance.
              """
              def agg(vs: list[T]) -> (Optional[T] if alt is None else T):
                  return vs[index] if len(vs) > index else alt
              #return self.fold(lambda vs: vs[index] if len(vs) > index else alt)
              return self.fold(agg)

          def head(self, alt: Any = None) -> 'NodeSerializer':
              """
              Set an *aggregator* which picks up the first node.

              Args:
                  alt: A value used when there exists no nodes.
              Returns:
                  This instance.
              """
              return self.at(0, alt)

          def last(self, alt: Any = None) -> 'NodeSerializer':
              """
              Set an *aggregator* which picks up the last node.

              Args:
                  alt: A value used when there exists no nodes.
              Returns:
                  This instance.
              """
              def agg(vs: list[T]) -> (Optional[T] if alt is None else T):
                  return vs[-1] if len(vs) > 0 else alt
              #return self.fold(lambda vs: vs[-1] if len(vs) > 0 else alt)
              return self.fold(agg)

          def fold(self, aggregator: Callable[[list[Node]], Any]) -> 'NodeSerializer':
              """
              Set an aggregation function converting a list of nodes into a single node or any value.

              Args:
                  aggregator: An aggregation function.
              Returns:
                  This instance.
              """
              return self._set_aggregator(aggregator, True)

          def select(self, aggregator: Callable[[list[Node]], list[Node]]) -> 'NodeSerializer':
              """
              Set an aggregation function selecting a list of nodes from all nodes from the container.

              Args:
                  aggregator: An aggregation function.
              Returns:
                  This instance.
              """
              return self._set_aggregator(aggregator, False)

          #----------------------------------------------------------------
          # Serizlization
          #----------------------------------------------------------------
          def each(self, func: Serializer) -> 'NodeSerializer':
              """
              Set a *serializer* .

              *serializer* is a function which will be invoked with a single argument of `NodeContext` ,
              from which internal code of *serializer* can get information of the node.

              For the sake of static typing, the *serializer* should have correct returns annotation.

              Args:
                  func: A function converting a node entity into a value.
              Returns:
                  This instance.
              """
              self._serializers.append(func)
              return self

          def sub(self, **settings) -> 'NodeSerializer':
              """
              Set serialization settings for sub graph.

              This method is used for the property whose kind is `GraphTemplate` .
              The form of `settings` is same as keyword arguments used to serialize the graph.

              Args:
                  settings: Serialization settings used to serialize sub graph.
              Returns:
                  This instance.
              """
              from pyracmon.graph.schema import GraphSchema
              class SubGraph(Typeable[T]):
                  serializers = settings.copy()

                  @staticmethod
                  def resolve(sub_graph, bound, arg, spec):
                      return GraphSchema(spec, arg.template, **sub_graph.serializers).schema

              def to_dict(cxt: NodeContext) -> SubGraph[T]:
                  vv = cxt.serialize()
                  return SerializationContext(
                      settings,
                      cxt.context.finder if cxt.context else lambda t: [],
                  ).execute(vv.view) # type: ignore

              self._serializers.append(to_dict)
              return self.head()

          def alter(
              self,
              generator: Optional[Serializer] = None,
              excludes: Optional[Iterable[str]] = None,
              includes: Optional[Iterable[str]] = None,
          ) -> 'NodeSerializer':
              """
              Extends and shrinks the dictionary obtained as a result of *serializer*s applied beforehand.

              `generator` is a kind of *serializer* returning `dict` which will be merged into serialization result.
              This can be used to add extra key value pairs into the result.
              For the sake of static typing, `generator` should have correct return annotation of `TypedDict` .

              `excludes` and `includes` are used to select keys from the result.

              Args:
                  generator: A function generating dictionary to be merged.
                  excludes: Keys to exclude.
                  includes: Keys to keep.
              Returns:
                  This instance.
              """
              excludes = excludes or []

              class EachExtend(Extend[T]):
                  @classmethod
                  def schema(cls, bound, arg):
                      return signature(generator).return_annotation if generator else Signature.empty

              class EachShrink(Shrink[T]):
                  @classmethod
                  def select(cls, td, bound):
                      return excludes, includes

              def convert(cxt) -> EachShrink[EachExtend[T]]:
                  ext = generator(cxt) if generator else {}
                  vv = cxt.serialize()
                  vv.update(**to_rawdict(ext, True))
                  return {k:v for k, v in vv.items() if (not includes or k in includes) and k not in excludes} # type: ignore

              self._serializers.append(convert)
              return self

  ### Ancestors

  - NodeSerializing
  - typing.Protocol
  - typing.Generic

  ### Instance variables

  `var aggregator : Callable[[list[Node]], Union[list[Node], Node, Any]]`

    Returns aggregator supplied with correct return annotation.

    Expand source code

        @property
        def aggregator(self) -> Callable[[list[Node]], Union[list[Node], Node, Any]]:
            """
            Returns *aggregator* supplied with correct return annotation.
            """
            if self._aggregator is None:
                def agg1(values: list[T]) -> list[T]:
                    return values
                return agg1
            elif signature(self._aggregator).return_annotation == Signature.empty:
                # TODO: No return annotation implies list to list aggregation.
                def agg2(values: list[T]) -> list[T]:
                    return self._aggregator(values) # type: ignore
                return agg2
            else:
                return self._aggregator

  `var be_merged : bool`

    Returns whether the converted value will be merged into parent.

    Expand source code

        @property
        def be_merged(self) -> bool:
            """
            Returns whether the converted value will be merged into parent.
            """
            return self._be_merged

  `var be_singular : bool`

    Returns whether the converted value will be a singular object, not a list.

    This is estimated by annotation of aggregation function. If its returning type is not annotated, this property always returns `False` . Builder methods adds appropriate annotation to given function when it does not have the annotation.

    Expand source code

        @property
        def be_singular(self) -> bool:
            """
            Returns whether the converted value will be a singular object, not a list.

            This is estimated by annotation of aggregation function. If its returning type is not annotated, this property always returns `False` .
            Builder methods adds appropriate annotation to given function when it does not have the annotation.
            """
            rt = signature(self.aggregator).return_annotation
            return not issubgeneric(rt, list)

  `var namer : Callable[[str], str]`

    Returns namer in the form of function even when not to merge.

    Expand source code

        @property
        def namer(self) -> Callable[[str], str]:
            """
            Returns *namer* in the form of function even when not to merge.
            """
            def f(v: str) -> str:
                if self._namer is None:
                    return v
                elif isinstance(self._namer, str):
                    return self._namer
                else:
                    return self._namer(v)
            return f

  `var serializer : Callable[[NodeContext], Any]`

    Returns merged serializer which has correctly annotated signature.

    Expand source code

        @property
        def serializer(self) -> Serializer:
            """
            Returns merged *serializer* which has correctly annotated signature.
            """
            return chain_serializers(self._serializers)

  ### Methods

  `def alter(self, generator: Optional[Callable[[ForwardRef('NodeContext')], Any]] = None, excludes: Optional[collections.abc.Iterable[str]] = None, includes: Optional[collections.abc.Iterable[str]] = None) ‑> NodeSerializer`

    Extends and shrinks the dictionary obtained as a result of serializers applied beforehand.

    `generator` is a kind of serializer returning `dict` which will be merged into serialization result. This can be used to add extra key value pairs into the result. For the sake of static typing, `generator` should have correct return annotation of `TypedDict` .

    `excludes` and `includes` are used to select keys from the result.

    ## Args

    `generator`
      A function generating dictionary to be merged.
    `excludes`
      Keys to exclude.
    `includes`
      Keys to keep.

    ## Returns

    This instance.

    Expand source code

        def alter(
            self,
            generator: Optional[Serializer] = None,
            excludes: Optional[Iterable[str]] = None,
            includes: Optional[Iterable[str]] = None,
        ) -> 'NodeSerializer':
            """
            Extends and shrinks the dictionary obtained as a result of *serializer*s applied beforehand.

            `generator` is a kind of *serializer* returning `dict` which will be merged into serialization result.
            This can be used to add extra key value pairs into the result.
            For the sake of static typing, `generator` should have correct return annotation of `TypedDict` .

            `excludes` and `includes` are used to select keys from the result.

            Args:
                generator: A function generating dictionary to be merged.
                excludes: Keys to exclude.
                includes: Keys to keep.
            Returns:
                This instance.
            """
            excludes = excludes or []

            class EachExtend(Extend[T]):
                @classmethod
                def schema(cls, bound, arg):
                    return signature(generator).return_annotation if generator else Signature.empty

            class EachShrink(Shrink[T]):
                @classmethod
                def select(cls, td, bound):
                    return excludes, includes

            def convert(cxt) -> EachShrink[EachExtend[T]]:
                ext = generator(cxt) if generator else {}
                vv = cxt.serialize()
                vv.update(**to_rawdict(ext, True))
                return {k:v for k, v in vv.items() if (not includes or k in includes) and k not in excludes} # type: ignore

            self._serializers.append(convert)
            return self

  `def at(self, index: int, alt: Any = None) ‑> NodeSerializer`

    Set an aggregator which picks up the node at the index.

    ## Args

    `index`
      An index of the element.
    `alt`
      A value used when no node is found at the index.

    ## Returns

    This instance.

    Expand source code

        def at(self, index: int, alt: Any = None) -> 'NodeSerializer':
            """
            Set an *aggregator* which picks up the node at the index.

            Args:
                index: An index of the element.
                alt: A value used when no node is found at the index.
            Returns:
                This instance.
            """
            def agg(vs: list[T]) -> (Optional[T] if alt is None else T):
                return vs[index] if len(vs) > index else alt
            #return self.fold(lambda vs: vs[index] if len(vs) > index else alt)
            return self.fold(agg)

  `def doc(self, document: str, **options: Any) ‑> NodeSerializer`

    Set the documentation for this node.

    `document` is used in graph schema as a parameter of `Annotated` .

    ## Args

    `document`
      A documentation string.
    `options`
      Documentation options.

    ## Returns

    This instance.

    Expand source code

        def doc(self, document: str, **options: Any) -> 'NodeSerializer':
            """
            Set the documentation for this node.

            `document` is used in graph schema as a parameter of `Annotated` .

            Args:
                document: A documentation string.
                options: Documentation options.
            Returns:
                This instance.
            """
            self._doc = document
            self._doc_options = options
            return self

  `def each(self, func: Callable[[ForwardRef('NodeContext')], Any]) ‑> NodeSerializer`

    Set a serializer .

    serializer is a function which will be invoked with a single argument of `NodeContext` , from which internal code of serializer can get information of the node.

    For the sake of static typing, the serializer should have correct returns annotation.

    ## Args

    `func`
      A function converting a node entity into a value.

    ## Returns

    This instance.

    Expand source code

        def each(self, func: Serializer) -> 'NodeSerializer':
            """
            Set a *serializer* .

            *serializer* is a function which will be invoked with a single argument of `NodeContext` ,
            from which internal code of *serializer* can get information of the node.

            For the sake of static typing, the *serializer* should have correct returns annotation.

            Args:
                func: A function converting a node entity into a value.
            Returns:
                This instance.
            """
            self._serializers.append(func)
            return self

  `def fold(self, aggregator: Callable[[list[Node]], Any]) ‑> NodeSerializer`

    Set an aggregation function converting a list of nodes into a single node or any value.

    ## Args

    `aggregator`
      An aggregation function.

    ## Returns

    This instance.

    Expand source code

        def fold(self, aggregator: Callable[[list[Node]], Any]) -> 'NodeSerializer':
            """
            Set an aggregation function converting a list of nodes into a single node or any value.

            Args:
                aggregator: An aggregation function.
            Returns:
                This instance.
            """
            return self._set_aggregator(aggregator, True)

  `def head(self, alt: Any = None) ‑> NodeSerializer`

    Set an aggregator which picks up the first node.

    ## Args

    `alt`
      A value used when there exists no nodes.

    ## Returns

    This instance.

    Expand source code

        def head(self, alt: Any = None) -> 'NodeSerializer':
            """
            Set an *aggregator* which picks up the first node.

            Args:
                alt: A value used when there exists no nodes.
            Returns:
                This instance.
            """
            return self.at(0, alt)

  `def last(self, alt: Any = None) ‑> NodeSerializer`

    Set an aggregator which picks up the last node.

    ## Args

    `alt`
      A value used when there exists no nodes.

    ## Returns

    This instance.

    Expand source code

        def last(self, alt: Any = None) -> 'NodeSerializer':
            """
            Set an *aggregator* which picks up the last node.

            Args:
                alt: A value used when there exists no nodes.
            Returns:
                This instance.
            """
            def agg(vs: list[T]) -> (Optional[T] if alt is None else T):
                return vs[-1] if len(vs) > 0 else alt
            #return self.fold(lambda vs: vs[-1] if len(vs) > 0 else alt)
            return self.fold(agg)

  `def merge(self, namer: Optional[Callable[[str], str]] = None) ‑> NodeSerializer`

    Set a naming function for merging into parent.

    Because merging needs folding, this method overrides the aggregation function by invoking `head()` internally if this instance is not configured to fold nodes into a single node.

    ## Args

    `namer`
      The naming function. If `None`, the property is used as it is.

    ## Returns

    This instance.

    Expand source code

        def merge(self, namer: Optional[Callable[[str], str]] = None) -> 'NodeSerializer':
            """
            Set a naming function for merging into parent.

            Because merging needs folding, this method overrides the aggregation function by invoking `head()` internally
            if this instance is not configured to fold nodes into a single node.

            Args:
                namer: The naming function. If `None`, the property is used as it is.
            Returns:
                This instance.
            """
            if namer and not callable(namer):
                raise ValueError(f"The method merging a node into its parent node must be callable or None.")
            self._namer = namer or (lambda x:x)
            self._be_merged = True
            if not self.be_singular:
                self.head()
            return self

  `def name(self, name: str) ‑> NodeSerializer`

    Set a key in parent dictionary.

    ## Args

    `name`
      A key string.

    ## Returns

    This instance.

    Expand source code

        def name(self, name: str) -> 'NodeSerializer':
            """
            Set a key in parent dictionary.

            Args:
                name: A key string.
            Returns:
                This instance.
            """
            if not isinstance(name, str):
                raise ValueError(f"The name of node must be a string but {type(name)} is given.")
            self._namer = name
            self._be_merged = False
            return self

  `def select(self, aggregator: Callable[[list[Node]], list[Node]]) ‑> NodeSerializer`

    Set an aggregation function selecting a list of nodes from all nodes from the container.

    ## Args

    `aggregator`
      An aggregation function.

    ## Returns

    This instance.

    Expand source code

        def select(self, aggregator: Callable[[list[Node]], list[Node]]) -> 'NodeSerializer':
            """
            Set an aggregation function selecting a list of nodes from all nodes from the container.

            Args:
                aggregator: An aggregation function.
            Returns:
                This instance.
            """
            return self._set_aggregator(aggregator, False)

  `def sub(self, **settings) ‑> NodeSerializer`

    Set serialization settings for sub graph.

    This method is used for the property whose kind is `GraphTemplate` . The form of `settings` is same as keyword arguments used to serialize the graph.

    ## Args

    `settings`
      Serialization settings used to serialize sub graph.

    ## Returns

    This instance.

    Expand source code

        def sub(self, **settings) -> 'NodeSerializer':
            """
            Set serialization settings for sub graph.

            This method is used for the property whose kind is `GraphTemplate` .
            The form of `settings` is same as keyword arguments used to serialize the graph.

            Args:
                settings: Serialization settings used to serialize sub graph.
            Returns:
                This instance.
            """
            from pyracmon.graph.schema import GraphSchema
            class SubGraph(Typeable[T]):
                serializers = settings.copy()

                @staticmethod
                def resolve(sub_graph, bound, arg, spec):
                    return GraphSchema(spec, arg.template, **sub_graph.serializers).schema

            def to_dict(cxt: NodeContext) -> SubGraph[T]:
                vv = cxt.serialize()
                return SerializationContext(
                    settings,
                    cxt.context.finder if cxt.context else lambda t: [],
                ).execute(vv.view) # type: ignore

            self._serializers.append(to_dict)
            return self.head()

`class NodeSerializing (*args, **kwargs)`

  Base class for protocol classes.

  Protocol classes are defined as::

      class Proto(Protocol):
          def meth(self) -> int:
              ...

  Such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing), for example::

      class C:
          def meth(self) -> int:
              return 0

      def func(x: Proto) -> int:
          return x.meth()

      func(C())  # Passes static type check

  See PEP 544 for details. Protocol classes decorated with @typing.runtime_checkable act as simple-minded runtime protocols that check only the presence of given attributes, ignoring their type signatures. Protocol classes can be generic, they are defined as::

      class GenProto(Protocol[T]):
          def meth(self) -> T:
              ...

  Expand source code

      class NodeSerializing(Protocol):
          def doc(self, document: str, **options: Any) -> 'NodeSerializer': ...
          def name(self, name: str) -> 'NodeSerializer': ...
          def merge(self, namer: Optional[Callable[[str], str]] = None) -> 'NodeSerializer': ...
          def at(self, index: int, alt: Any = None) -> 'NodeSerializer': ...
          def head(self, alt: Any = None) -> 'NodeSerializer': ...
          def last(self, alt: Any = None) -> 'NodeSerializer': ...
          def fold(self, aggregator: Callable[[list[Node]], Any]) -> 'NodeSerializer': ...
          def select(self, aggregator: Callable[[list[Node]], list[Node]]) -> 'NodeSerializer': ...
          def each(self, func: Serializer) -> 'NodeSerializer': ...
          def sub(self, **settings) -> 'NodeSerializer': ...
          def alter(
              self,
              generator: Optional[Serializer] = None,
              excludes: Optional[Iterable[str]] = None,
              includes: Optional[Iterable[str]] = None,
          ) -> 'NodeSerializer': ...

  ### Ancestors

  - typing.Protocol
  - typing.Generic

  ### Subclasses

  - NodeSerializer
  - SerializerMeta

  ### Methods

  `def alter(self, generator: Optional[Callable[[ForwardRef('NodeContext')], Any]] = None, excludes: Optional[collections.abc.Iterable[str]] = None, includes: Optional[collections.abc.Iterable[str]] = None) ‑> NodeSerializer`
    Expand source code

        def alter(
            self,
            generator: Optional[Serializer] = None,
            excludes: Optional[Iterable[str]] = None,
            includes: Optional[Iterable[str]] = None,
        ) -> 'NodeSerializer': ...

  `def at(self, index: int, alt: Any = None) ‑> NodeSerializer`
    Expand source code

        def at(self, index: int, alt: Any = None) -> 'NodeSerializer': ...

  `def doc(self, document: str, **options: Any) ‑> NodeSerializer`
    Expand source code

        def doc(self, document: str, **options: Any) -> 'NodeSerializer': ...

  `def each(self, func: Callable[[ForwardRef('NodeContext')], Any]) ‑> NodeSerializer`
    Expand source code

        def each(self, func: Serializer) -> 'NodeSerializer': ...

  `def fold(self, aggregator: Callable[[list[Node]], Any]) ‑> NodeSerializer`
    Expand source code

        def fold(self, aggregator: Callable[[list[Node]], Any]) -> 'NodeSerializer': ...

  `def head(self, alt: Any = None) ‑> NodeSerializer`
    Expand source code

        def head(self, alt: Any = None) -> 'NodeSerializer': ...

  `def last(self, alt: Any = None) ‑> NodeSerializer`
    Expand source code

        def last(self, alt: Any = None) -> 'NodeSerializer': ...

  `def merge(self, namer: Optional[Callable[[str], str]] = None) ‑> NodeSerializer`
    Expand source code

        def merge(self, namer: Optional[Callable[[str], str]] = None) -> 'NodeSerializer': ...

  `def name(self, name: str) ‑> NodeSerializer`
    Expand source code

        def name(self, name: str) -> 'NodeSerializer': ...

  `def select(self, aggregator: Callable[[list[Node]], list[Node]]) ‑> NodeSerializer`
    Expand source code

        def select(self, aggregator: Callable[[list[Node]], list[Node]]) -> 'NodeSerializer': ...

  `def sub(self, **settings) ‑> NodeSerializer`
    Expand source code

        def sub(self, **settings) -> 'NodeSerializer': ...

`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 `NodeSerializer`s 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)

`class SerializationContext (settings: dict[str, NodeSerializer], finder: Callable[[type], list[Callable[[ForwardRef('NodeContext')], Any]]], node_params: Optional[dict[str, dict[str, typing.Any]]] = None)`

  This class implements actual serialization flow applied to a graph.

  `node_params` is a `dict` whose values will be passed to serializer via `params` attribute of `NodeContext` . The property name of the node is used to get values (also a `dict` ) from the `dict` and the `params` exposes them as its attributes of the same names as their keys.

  Arbitrary values can be passed in `node_params` which is a `dict`

  Following code shows the example passing a parameter to a serializer .

      cxt = SerializationContext(
          dict(
              a = S.each(lambda cxt: cxt.value*c.params.value),
          ),
          finder,
          dict(a={"value": 10})
      )

  ## Args

  `settings`
    Mapping of node name to `NodeSerializer` .
  `finder`
    A function to find base serializer s by a `type` .
  `node_params`
    Arbitrary parameters passed to serializers.
  Expand source code

      class SerializationContext:
          """
          This class implements actual serialization flow applied to a graph.

          `node_params` is a `dict` whose values will be passed to *serializer* via `params` attribute of `NodeContext` .
          The property name of the node is used to get values (also a `dict` ) from the `dict`
          and the `params` exposes them as its attributes of the same names as their keys.

          Arbitrary values can be passed in `node_params` which is a `dict`

          Following code shows the example passing a parameter to a *serializer* .

          ```python
          cxt = SerializationContext(
              dict(
                  a = S.each(lambda cxt: cxt.value*c.params.value),
              ),
              finder,
              dict(a={"value": 10})
          )
          ```

          Args:
              settings: Mapping of node name to `NodeSerializer` .
              finder: A function to find base *serializer* s by a `type` .
              node_params: Arbitrary parameters passed to *serializer*s.
          """
          def __init__(
              self,
              settings: dict[str, NodeSerializer],
              finder: Callable[[type], list[Serializer]],
              node_params: Optional[dict[str, dict[str, Any]]] = None,
          ):
              self.settings: dict[str, NodeSerializer] = settings
              self.finder: Callable[[type], list[Serializer]] = finder
              self._node_params: dict[str, dict[str, Any]] = node_params or {}
              self._context_factories: dict[str, NodeContextFactory] = {}

          def __getitem__(self, node: Union[Node, str]) -> Any:
              """
              Returns an accessor to parameters for given node.

              Args:
                  node: Node or node name.
              Returns:
                  An object exposing parameters for the node as attributes.
              """
              name = node.name if isinstance(node, Node) else node
              params = self._node_params.get(name, {})

              class Accessor:
                  def __getattr__(self, key):
                      return params.get(key, None)

              return Accessor()

          def execute(self, graph: GraphView) -> dict[str, Any]:
              """
              Serializes a graph.

              Args:
                  graph: The view of graph to serialize.
              Returns:
                  Serialization result.
              """
              result = {}
              for c in graph().roots:
                  self.serialize_to(c.name, c, result)
              return result

          def serialize_to(self, name: str, container: Union[NodeContainer, Node.Children], parent: dict[str, Any]) -> None:
              """
              Serialize nodes and appends them into the dictionary.

              Args:
                  name: Name of the template property associated with the nodes.
                  container: Container of nodes.
                  parent: A parent dictionary to which serialized values will be appended.
              """
              ns = self.settings.get(name, None)

              if not ns:
                  # Nodes whose names are not supplied to settings are not serialized.
                  return

              # First, aggregate nodes into its subset or a single node.
              nodes: Union[list[Node], Node, Any] = ns.aggregator(container.nodes)

              if ns.be_singular:
                  if isinstance(nodes, list):
                      raise ValueError(f"Aggregation function is marked to create a single value but returns node list.")

                  # Alternative value given to aggregation function may be returned instead of node.
                  value = self._serialize_node(nodes, ns) if isinstance(nodes, Node) else nodes

                  if ns.be_merged:
                      if value is None:
                          # When empty, no key-value pair is added to parent.
                          return
                      elif not isinstance(value, dict):
                          raise ValueError(f"Serialized value must be dict but {type(value)}.")

                      parent.update({ns.namer(k):v for k, v in value.items()})
                  else:
                      parent[ns.namer(name)] = value
              else:
                  if not isinstance(nodes, list):
                      raise ValueError(f"Aggregation function is marked to return node list but returns a single value.")
                  if ns.be_merged:
                      raise ValueError(f"Merging to parent dict requires folding.")

                  parent[ns.namer(name)] = [self._serialize_node(n, ns) for n in nodes]

          def _find_serializer(self, prop: GraphTemplate.Property) -> list[Serializer]:
              return self.finder(prop.kind) if isinstance(prop.kind, type) else []

          def _serialize_node(self, node: Node, node_serializer: NodeSerializer):
              if not node.prop.name in self._context_factories:
                  self._context_factories[node.prop.name] = NodeContextFactory(
                      self,
                      self._find_serializer(node.prop),
                      self._node_params.get(node.prop.name, {}),
                  )

              factory = self._context_factories[node.prop.name]

              with factory.begin(node, node_serializer._serializers) as cxt:
                  value = cxt.serialize()

                  # Child nodes are serialized only when the parent node is serialized into a dict.
                  if isinstance(value, dict):
                      for n, ch in node.children.items():
                          self.serialize_to(n, ch, value)

                  return value

  ### Methods

  `def execute(self, graph: GraphView) ‑> dict[str, typing.Any]`

    Serializes a graph.

    ## Args

    `graph`
      The view of graph to serialize.

    ## Returns

    Serialization result.

    Expand source code

        def execute(self, graph: GraphView) -> dict[str, Any]:
            """
            Serializes a graph.

            Args:
                graph: The view of graph to serialize.
            Returns:
                Serialization result.
            """
            result = {}
            for c in graph().roots:
                self.serialize_to(c.name, c, result)
            return result

  `def serialize_to(self, name: str, container: Union[NodeContainer, Node.Children], parent: dict[str, typing.Any]) ‑> None`

    Serialize nodes and appends them into the dictionary.

    ## Args

    `name`
      Name of the template property associated with the nodes.
    `container`
      Container of nodes.
    `parent`
      A parent dictionary to which serialized values will be appended.
    Expand source code

        def serialize_to(self, name: str, container: Union[NodeContainer, Node.Children], parent: dict[str, Any]) -> None:
            """
            Serialize nodes and appends them into the dictionary.

            Args:
                name: Name of the template property associated with the nodes.
                container: Container of nodes.
                parent: A parent dictionary to which serialized values will be appended.
            """
            ns = self.settings.get(name, None)

            if not ns:
                # Nodes whose names are not supplied to settings are not serialized.
                return

            # First, aggregate nodes into its subset or a single node.
            nodes: Union[list[Node], Node, Any] = ns.aggregator(container.nodes)

            if ns.be_singular:
                if isinstance(nodes, list):
                    raise ValueError(f"Aggregation function is marked to create a single value but returns node list.")

                # Alternative value given to aggregation function may be returned instead of node.
                value = self._serialize_node(nodes, ns) if isinstance(nodes, Node) else nodes

                if ns.be_merged:
                    if value is None:
                        # When empty, no key-value pair is added to parent.
                        return
                    elif not isinstance(value, dict):
                        raise ValueError(f"Serialized value must be dict but {type(value)}.")

                    parent.update({ns.namer(k):v for k, v in value.items()})
                else:
                    parent[ns.namer(name)] = value
            else:
                if not isinstance(nodes, list):
                    raise ValueError(f"Aggregation function is marked to return node list but returns a single value.")
                if ns.be_merged:
                    raise ValueError(f"Merging to parent dict requires folding.")

                parent[ns.namer(name)] = [self._serialize_node(n, ns) for n in nodes]

`class SerializerMeta (*args, **kwargs)`

  Base class for protocol classes.

  Protocol classes are defined as::

      class Proto(Protocol):
          def meth(self) -> int:
              ...

  Such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing), for example::

      class C:
          def meth(self) -> int:
              return 0

      def func(x: Proto) -> int:
          return x.meth()

      func(C())  # Passes static type check

  See PEP 544 for details. Protocol classes decorated with @typing.runtime_checkable act as simple-minded runtime protocols that check only the presence of given attributes, ignoring their type signatures. Protocol classes can be generic, they are defined as::

      class GenProto(Protocol[T]):
          def meth(self) -> T:
              ...

  Expand source code

      class SerializerMeta(NodeSerializing, type): # type: ignore
          @classmethod
          def __prepare__(cls, __name: str, __bases: tuple[type, ...], **kwds: Any) -> Mapping[str, object]:
              def wrap(n, f):
                  def g(*args, **kwargs):
                      ns = NodeSerializer()
                      nf = getattr(ns, n)
                      return nf(*args, **kwargs)
                  return g
              return {n: wrap(n, f) for n, f in getmembers(NodeSerializing, isfunction) if not n.startswith("__")}

  ### Ancestors

  - NodeSerializing
  - typing.Protocol
  - typing.Generic
  - builtins.type