Applying decorators#

When using decorators with providers, care must be taken to allow Sciline to recognize the argument and return types. This is done easiest with functools.wraps. The following decorator can be safely applied to providers:

[1]:
import functools
from typing import Any, TypeVar
from collections.abc import Callable

import sciline

R = TypeVar('R')


def deco(f: Callable[..., R]) -> Callable[..., R]:
    @functools.wraps(f)
    def impl(*args: Any, **kwargs: Any) -> R:
        return f(*args, **kwargs)

    return impl


@deco
def to_string(x: int) -> str:
    return str(x)


pipeline = sciline.Pipeline([to_string], params={int: 3})
pipeline.compute(str)
[1]:
'3'

Omitting functools.wraps results in an error when computing results:

[2]:
def bad_deco(f: Callable[..., R]) -> Callable[..., R]:
    def impl(*args: Any, **kwargs: Any) -> R:
        return f(*args, **kwargs)

    return impl


@bad_deco
def to_string(x: int) -> str:
    return str(x)


pipeline = sciline.Pipeline([to_string], params={int: 3})
pipeline.compute(str)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[2], line 13
      8 @bad_deco
      9 def to_string(x: int) -> str:
     10     return str(x)
---> 13 pipeline = sciline.Pipeline([to_string], params={int: 3})
     14 pipeline.compute(str)

File ~/work/sciline/sciline/.tox/docs/lib/python3.10/site-packages/sciline/pipeline.py:142, in Pipeline.__init__(self, providers, params, constraints)
    117 def __init__(
    118     self,
    119     providers: Iterable[ToProvider | Provider] | None = None,
   (...)
    122     constraints: Mapping[TypeVar, Iterable[Key]] | None = None,
    123 ):
    124     """
    125     Setup a Pipeline from a list providers
    126
   (...)
    140         the type variable definition.
    141     """
--> 142     super().__init__(providers, constraints=constraints)
    143     for tp, param in (params or {}).items():
    144         self[tp] = param

File ~/work/sciline/sciline/.tox/docs/lib/python3.10/site-packages/sciline/data_graph.py:93, in DataGraph.__init__(self, providers, constraints)
     91 self._cbgraph = cb.Graph(nx.DiGraph())
     92 for provider in providers or []:
---> 93     self.insert(provider)

File ~/work/sciline/sciline/.tox/docs/lib/python3.10/site-packages/sciline/data_graph.py:154, in DataGraph.insert(self, provider)
    152 return_type = provider.deduce_key()
    153 if typevars := _find_all_typevars(return_type):
--> 154     for bound in _mapping_to_constrained(typevars, self._constraints):
    155         self.insert(provider.bind_type_vars(bound))
    156     return

File ~/work/sciline/sciline/.tox/docs/lib/python3.10/site-packages/sciline/data_graph.py:55, in _mapping_to_constrained(type_vars, over_constraints)
     52 def _mapping_to_constrained(
     53     type_vars: set[TypeVar], over_constraints: dict[TypeVar, frozenset[Key]]
     54 ) -> Generator[dict[TypeVar, Key], None, None]:
---> 55     constraints = [_get_typevar_constraints(t, over_constraints) for t in type_vars]
     56     for combination in itertools.product(*constraints):
     57         yield dict(zip(type_vars, combination, strict=True))

File ~/work/sciline/sciline/.tox/docs/lib/python3.10/site-packages/sciline/data_graph.py:55, in <listcomp>(.0)
     52 def _mapping_to_constrained(
     53     type_vars: set[TypeVar], over_constraints: dict[TypeVar, frozenset[Key]]
     54 ) -> Generator[dict[TypeVar, Key], None, None]:
---> 55     constraints = [_get_typevar_constraints(t, over_constraints) for t in type_vars]
     56     for combination in itertools.product(*constraints):
     57         yield dict(zip(type_vars, combination, strict=True))

File ~/work/sciline/sciline/.tox/docs/lib/python3.10/site-packages/sciline/data_graph.py:45, in _get_typevar_constraints(t, over_constraints)
     43     return override
     44 if not (constraints := t.__constraints__):
---> 45     raise ValueError(
     46         f"Type variable {t!r} has no constraints. Either constrain the type "
     47         f"variable in its definition or via the 'constraints' argument of Pipeline."
     48     )
     49 return frozenset(constraints)

ValueError: Type variable ~R has no constraints. Either constrain the type variable in its definition or via the 'constraints' argument of Pipeline.

Hint

For Python 3.10+, the decorator itself can be type-annoted like this:

from typing import ParamSpec, TypeVar

P = ParamSpec('P')
R = TypeVar('R')

def deco(f: Callable[P, R]) -> Callable[P, R]:
    @functools.wraps(f)
    def impl(*args: P.args, **kwargs: P.kwargs) -> R:
        return f(*args, **kwargs)

    return impl

This is good practice but not required by Sciline.