Skip to content

binding

Utilities for automatic unmarshalling of inputs according to a callable object's signature.

Typical Usage

>>> from typelib import binding
>>>
>>> def foo(val: int) -> int:
...     return val * 2
...
>>> bound = binding.bind(foo)
>>> bound("2")
4
>>> bound.call("3")
'33'

AbstractBinding

AbstractBinding(*, signature: Signature, binding: BindingT, varkwd: AbstractUnmarshaller | None = None, varpos: AbstractUnmarshaller | None = None, startpos: int | None = None)

Bases: ABC, Generic[P]

The abstract base class for all type-enforced bindings.

Note

"Bindings" are callables which leverage the type annotations in a signature to unmarshal inputs.

We differentiate each subclass based upon the possible combinations of parameter kinds:

  • Positional-only arguments
  • Keyword-only arguments
  • Positional-or-Keyword arguments
  • Variable-positional arguments (*args)
  • Variable-keyword arguments (**kwargs)

This allows us to micro-optimize the call for each subclass to exactly what is necessary for the that combination, which can lead to a significant speedup in hot loops.

Parameters:

  • signature (Signature) –

    The signature for the binding.

  • binding (BindingT) –

    A mapping of parameter names and positions to unmarshallers. This accounts for positional, keyword, or positional-or-keyword arguments.

  • varkwd (AbstractUnmarshaller | None, default: None ) –

    The unmarshaller for var-keyword arguments (**kwargs).

  • varpos (AbstractUnmarshaller | None, default: None ) –

    The unmarshaller for var-positional arguments (*args).

  • startpos (int | None, default: None ) –

    The start position of var-positional arguments (*args). This accounts for the fact that var-positional comes after positional-only.

Source code in src/typelib/binding.py
def __init__(
    self,
    *,
    signature: inspect.Signature,
    binding: BindingT,
    varkwd: unmarshals.AbstractUnmarshaller | None = None,
    varpos: unmarshals.AbstractUnmarshaller | None = None,
    startpos: int | None = None,
):
    """Constructor.

    Args:
        signature: The signature for the binding.
        binding: A mapping of parameter names and positions to unmarshallers.
                 This accounts for positional, keyword, or positional-or-keyword arguments.
        varkwd: The unmarshaller for var-keyword arguments (`**kwargs`).
        varpos: The unmarshaller for var-positional arguments (`*args`).
        startpos: The start position of var-positional arguments (`*args`).
                  This accounts for the fact that var-positional comes after positional-only.
    """
    self.signature = signature
    self.binding = binding
    self.varkwd = varkwd
    self.varpos = varpos
    self.startpos = startpos

__call__ abstractmethod

__call__(args: tuple[Any], kwargs: dict[str, Any]) -> tuple[args, kwargs]

Inspect the given args and kwargs and unmarshal them.

Parameters:

  • args (tuple[Any]) –

    The positional arguments.

  • kwargs (dict[str, Any]) –

    The keyword arguments.

Source code in src/typelib/binding.py
@abc.abstractmethod
def __call__(
    self, args: tuple[tp.Any], kwargs: dict[str, tp.Any]
) -> tuple[P.args, P.kwargs]:
    """Inspect the given `args` and `kwargs` and unmarshal them.

    Args:
        args: The positional arguments.
        kwargs: The keyword arguments.
    """

BoundRoutine dataclass

BoundRoutine(call: Callable[P, R], binding: AbstractBinding[P])

Bases: Generic[P, R]

A type-enforced, bound routine for a callable object.

binding instance-attribute

binding: AbstractBinding[P]

The parameter->type binding.

call instance-attribute

call: Callable[P, R]

The callable object.

__call__

__call__(*args: Any, **kwargs: Any) -> R

Binding an input to the parameters of call,

then call the callable and return the result.

Source code in src/typelib/binding.py
def __call__(self, *args: tp.Any, **kwargs: tp.Any) -> R:
    """Binding an input to the parameters of `call`,

    then call the callable and return the result."""
    bargs, bkwargs = self.binding(args=args, kwargs=kwargs)
    return self.call(*bargs, **bkwargs)

bind

bind(obj: Callable[P, R]) -> BoundRoutine[P, R]

Create a type-enforced, bound routine for a callable object.

Note

In contrast to typelib.binding.wrap, this function creates a new, type-enforced BoundRoutine instance. Rather than masquerading as the given obj, we encapsulate it in the routine instance, which is more obvious and provides developers with the ability to side-step type enforcement when it is deemed unnecessary, which should be most of the time if your code is strongly typed and statically analyzed.

TL;DR

This function returns an object that walks like your duck and quacks like your duck, but doesn't look like your duck.

Parameters:

  • obj (Callable[P, R]) –

    The callable object to bind.

Source code in src/typelib/binding.py
def bind(obj: tp.Callable[P, R]) -> BoundRoutine[P, R]:
    """Create a type-enforced, bound routine for a callable object.

    Note:
        In contrast to [`typelib.binding.wrap`][], this function creates a new,
        type-enforced [`BoundRoutine`][typelib.binding.BoundRoutine] instance. Rather than
        masquerading as the given `obj`, we encapsulate it in the routine
        instance, which is more obvious and provides developers with the ability to
        side-step type enforcement when it is deemed unnecessary, which should be
        most of the time if your code is strongly typed and statically analyzed.

    Tip: TL;DR
        This function returns an object that walks like your duck and quacks like your duck,
        but doesn't look like your duck.

    Args:
        obj: The callable object to bind.
    """
    binding: AbstractBinding[P] = _get_binding(obj)
    routine: BoundRoutine[P, R] = BoundRoutine(
        call=obj,
        binding=binding,
    )
    return routine

wrap

wrap(obj: Callable[P, R]) -> Callable[..., R]

Wrap a callable object for runtime type coercion of inputs.

Note

If a class is given, we will attempt to wrap the init method.

Warning

This is a useful feature. It is also very surprising. By wrapping a callable in this decorator, we end up with implicit behavior that's not obvious to the caller or a fellow developer.

You're encouraged to prefer typelib.binding.bind for similar functionality, less the implicit nature, especially when a class is given.

TL;DR

This function returns an object walks, quacks, and tries to look like your duck.

Parameters:

  • obj (Callable[P, R]) –

    The callable object to wrap. Maybe be a function, a callable class instance, or a class.

Source code in src/typelib/binding.py
def wrap(obj: tp.Callable[P, R]) -> tp.Callable[..., R]:
    """Wrap a callable object for runtime type coercion of inputs.

    Note:
        If a class is given, we will attempt to wrap the init method.

    Warning:
        This is a useful feature. It is also very *surprising*. By wrapping a callable
        in this decorator, we end up with *implicit* behavior that's not obvious to the
        caller or a fellow developer.

        You're encouraged to prefer [`typelib.binding.bind`][] for similar
        functionality, less the implicit nature, especially when a class is given.

    Tip: TL;DR
        This function returns an object walks, quacks, and tries to look like your duck.

    Args:
        obj: The callable object to wrap.
             Maybe be a function, a callable class instance, or a class.
    """

    binding: AbstractBinding[P] = _get_binding(obj)

    if inspect.isclass(obj):
        obj.__init__ = wrap(obj.__init__)
        return obj

    @functools.wraps(obj)  # type: ignore[arg-type]
    def binding_wrapper(*args: tp.Any, __binding=binding, **kwargs: tp.Any) -> R:
        bargs, bkwargs = __binding(args, kwargs)
        return obj(*bargs, **bkwargs)

    return binding_wrapper