Source code for turbograph.core.specification

"""Define vertex specifications used to construct a dependency graph."""

from __future__ import annotations

import sys
from collections.abc import Callable, Iterable, Mapping, Sequence
from dataclasses import dataclass
from inspect import _ParameterKind, signature
from logging import getLogger
from typing import TYPE_CHECKING, Any, Generic, Union, cast

if sys.version_info >= (3, 11):
    from typing import TypeAlias
else:  # pragma: no cover
    from typing_extensions import TypeAlias


from .constant import NA, V, VertexFunc, VertexPredecessors, VertexValue

if TYPE_CHECKING:
    from .attribute import VertexAttributes

logger = getLogger(__name__)


def _get_arg_names(
    func: Callable[..., Any], kinds: Iterable[_ParameterKind]
) -> tuple[str, ...]:
    """Retrieve the names of arguments of a function, filtering by parameter kind.

    Args:
        func: The function whose arguments are being extracted.
        kinds: The kinds of arguments to include (e.g., positional-only, keyword-only).

    Returns:
        A tuple containing the names of the selected arguments.

    Example:
        >>> def example(a, b, *, c):
        ...     pass
        >>> _get_arg_names(example, kinds=[_ParameterKind.POSITIONAL_OR_KEYWORD])
        ('a', 'b')

    """
    if not kinds:
        return ()
    kinds = frozenset(kinds)
    sig = signature(func)
    return tuple(name for name, param in sig.parameters.items() if param.kind in kinds)


[docs] @dataclass(frozen=True) class VertexSpecification(Generic[V]): """Defines the specification for a vertex in a dependency graph.""" func: VertexFunc = None """An optional function to compute the vertex value. If ``None``, the vertex does not have an associated computation function. """ predecessors: VertexPredecessors[V] = () """A sequence of predecessor vertices providing inputs for computation.""" value: VertexValue = NA """The value of the vertex. The sentinel :py:data:`NA` is used to indicate that the value is missing or uncomputed. """
[docs] @classmethod def from_mapping(cls, value: Mapping[str, Any]) -> VertexSpecification[V]: """Create a vertex specification from a mapping. Args: value: A mapping containing the ``"func"``, ``"predecessors"``, and ``"value"`` keys. Returns: A :py:class:`VertexSpecification` instance. """ return cls(**value)
[docs] @classmethod def from_sequence(cls, value: Sequence[Any]) -> VertexSpecification[V]: """Create a vertex specification from a sequence. The sequence should follow the order: ``(func, predecessors, value)``. Args: value: A sequence containing function, predecessors, and value. Returns: A :py:class:`VertexSpecification` instance. """ return cls(*value)
[docs] @classmethod def from_func( cls, func: Callable[..., Any], *, kinds: Iterable[_ParameterKind] = () ) -> VertexSpecification[V]: """Create a vertex specification from a function. The function's parameter names are used as its predecessor vertices. Args: func: The function defining the vertex computation. kinds: The types of parameters to consider as predecessors. Returns: A :py:class:`VertexSpecification` with inferred predecessors. """ predecessors = _get_arg_names(func, kinds=kinds) logger.debug("Guessing predecessors %r for function %r", predecessors, func) return cls(func, predecessors) # type: ignore[arg-type]
[docs] @classmethod def from_value(cls, value: object) -> VertexSpecification[V]: """Create a vertex specification from a raw value. Args: value: A precomputed value. Returns: A :py:class:`VertexSpecification` with the value set. """ return cls(value=value)
[docs] @classmethod def from_any( cls, value: RawVertexSpecification[Any], *, kinds: Iterable[_ParameterKind] = () ) -> VertexSpecification[V]: """Create a vertex specification from a raw input of any supported type. Args: value: The raw input, which can be a dictionary, sequence, function, or value. kinds: The types of parameters to consider when inferring predecessors. Returns: A :py:class:`VertexSpecification` instance. """ if isinstance(value, Mapping): return cls.from_mapping(value) if isinstance(value, Sequence): return cls.from_sequence(value) if callable(value): return cls.from_func(value, kinds=kinds) return cls.from_value(value)
[docs] def to_dict(self) -> VertexAttributes[V]: """Convert the vertex specification into a dictionary of vertex attributes. Returns: A dictionary containing the ``"func"``, ``"predecessors"``, and ``"value"`` keys. """ return cast( "VertexAttributes[V]", {"func": self.func, "value": self.value, "predecessors": self.predecessors}, )
RawVertexSpecification: TypeAlias = Union[ # Already a VertexSpecification VertexSpecification[V], # A mapping: use `from_mapping` Mapping[str, Any], # A sequence: use `from_sequence` Sequence[Any], # A callable: use `from_func` Callable[..., Any], # A value: use `from_value` Any, ] """A raw vertex specification that can be converted into a :py:class:`VertexSpecification`. It can be one of the following types: - A :py:class:`VertexSpecification` (already structured). - A mapping (dictionary), processed with :py:meth:`VertexSpecification.from_mapping`. - A sequence (list, tuple), processed with :py:meth:`VertexSpecification.from_sequence`. - A callable function, processed with :py:meth:`VertexSpecification.from_func`. - Any other value, processed with :py:meth:`VertexSpecification.from_value`. """