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:72, in Pipeline.__init__(self, providers, params)
     55 def __init__(
     56     self,
     57     providers: Iterable[ToProvider | Provider] | None = None,
     58     *,
     59     params: dict[type[Any], Any] | None = None,
     60 ):
     61     """
     62     Setup a Pipeline from a list providers
     63
   (...)
     70         Dictionary of concrete values to provide for types.
     71     """
---> 72     super().__init__(providers)
     73     for tp, param in (params or {}).items():
     74         self[tp] = param

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

File ~/work/sciline/sciline/.tox/docs/lib/python3.10/site-packages/sciline/data_graph.py:114, in DataGraph.insert(self, provider)
    112 return_type = provider.deduce_key()
    113 if typevars := _find_all_typevars(return_type):
--> 114     for bound in _mapping_to_constrained(typevars):
    115         self.insert(provider.bind_type_vars(bound))
    116     return

File ~/work/sciline/sciline/.tox/docs/lib/python3.10/site-packages/sciline/data_graph.py:43, in _mapping_to_constrained(type_vars)
     41 constraints = [_get_typevar_constraints(t) for t in type_vars]
     42 if any(len(c) == 0 for c in constraints):
---> 43     raise ValueError('Typevars must have constraints')
     44 for combination in itertools.product(*constraints):
     45     yield dict(zip(type_vars, combination, strict=True))

ValueError: Typevars must have constraints

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.