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

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

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

File ~/work/sciline/sciline/.tox/docs/lib/python3.10/site-packages/sciline/data_graph.py:47, in _mapping_to_constrained(type_vars)
     45 constraints = [_get_typevar_constraints(t) for t in type_vars]
     46 if any(len(c) == 0 for c in constraints):
---> 47     raise ValueError('Typevars must have constraints')
     48 for combination in itertools.product(*constraints):
     49     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.