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
- context:
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
andselect
are the general methods to set aggregator toNodeSerializer
.>>> # 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 toNodeSerializer
directly byeach
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 thedict
with the same keys as their property names. The key can be changed by set namer to theNodeSerializer
byname
.Here,
merge
is a special configuration ofNodeSerializer
, which can be used for the case that a child node is also serialized intodict
and it is wanted to be merged into parentdict
. It can take a callable which converts key in original childdict
into another key used in parentdict
.Whether the child should be put or merge into parent
dict
is determined whether namer isstr
(orNone
) orCallable
.>>> 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 returningdict
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 ofTypedDict
.excludes
andincludes
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 ofAnnotated
.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 ofsettings
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
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 onNodeSerializer
.Use them to supply
NodeSerializer
s to functions to serialize a graph or to create a graph schema such asgraph_dict
orgraph_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 adict
whose values will be passed to serializer viaparams
attribute ofNodeContext
. The property name of the node is used to get values (also adict
) from thedict
and theparams
exposes them as its attributes of the same names as their keys.Arbitrary values can be passed in
node_params
which is adict
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