in_n_out-0.1.8/src/in_n_out/__init__.py0000644000000000000000000000210313615410400014760 0ustar00"""plugable dependency injection and result processing.""" from importlib.metadata import PackageNotFoundError, version try: __version__ = version("in-n-out") except PackageNotFoundError: # pragma: no cover __version__ = "uninstalled" __author__ = "Talley Lambert" __email__ = "talley.lambert@gmail.com" from ._global import ( inject, inject_processors, iter_processors, iter_providers, mark_processor, mark_provider, process, provide, register, register_processor, register_provider, ) from ._store import Store from ._type_resolution import ( resolve_single_type_hints, resolve_type_hints, type_resolved_signature, ) from ._util import _compiled __all__ = [ "register_provider", "_compiled", "inject", "iter_processors", "iter_providers", "inject_processors", "process", "mark_processor", "provide", "mark_provider", "register_processor", "register", "register", "resolve_single_type_hints", "resolve_type_hints", "Store", "type_resolved_signature", ] in_n_out-0.1.8/src/in_n_out/_global.py0000644000000000000000000001664513615410400014640 0ustar00from __future__ import annotations from textwrap import indent from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal, overload from ._store import InjectionContext, Store if TYPE_CHECKING: from ._store import ( P, Processor, ProcessorIterable, ProcessorVar, Provider, ProviderIterable, ProviderVar, R, T, ) from ._type_resolution import RaiseWarnReturnIgnore _STORE_PARAM = """ store : Union[Store, str None] The store instance or store name to use, if not provided the global store is used. """ _STORE_PARAM = indent(_STORE_PARAM.strip(), " ") def _add_store_to_doc(func: T) -> T: new_doc: list[str] = [] store_doc: str = getattr(Store, func.__name__).__doc__ # type: ignore for n, line in enumerate(store_doc.splitlines()): if line.lstrip().startswith("Returns"): new_doc.insert(n - 1, _STORE_PARAM) # TODO: use re.sub instead new_doc.append(line.replace(" store.", " ").replace("@store.", "@")) func.__doc__ = "\n".join(new_doc) return func def _store_or_global(store: str | Store | None = None) -> Store: return store if isinstance(store, Store) else Store.get_store(store) @_add_store_to_doc def register( *, processors: ProcessorIterable | None = None, providers: ProviderIterable | None = None, store: str | Store | None = None, ) -> InjectionContext: return _store_or_global(store).register(providers=providers, processors=processors) @_add_store_to_doc def register_provider( provider: Provider, type_hint: object | None = None, weight: float = 0, store: str | Store | None = None, ) -> InjectionContext: return _store_or_global(store).register_provider( provider=provider, type_hint=type_hint, weight=weight ) @_add_store_to_doc def register_processor( processor: Processor, type_hint: object | None = None, weight: float = 0, store: str | Store | None = None, ) -> InjectionContext: return _store_or_global(store).register_processor( processor=processor, type_hint=type_hint, weight=weight ) @overload def mark_provider( func: ProviderVar, *, weight: float = 0, type_hint: object | None = None, store: str | Store | None = None, ) -> ProviderVar: ... @overload def mark_provider( func: Literal[None] = ..., *, weight: float = 0, type_hint: object | None = None, store: str | Store | None = None, ) -> Callable[[ProviderVar], ProviderVar]: ... @_add_store_to_doc def mark_provider( func: ProviderVar | None = None, *, weight: float = 0, type_hint: object | None = None, store: str | Store | None = None, ) -> Callable[[ProviderVar], ProviderVar] | ProviderVar: return _store_or_global(store).mark_provider( func, weight=weight, type_hint=type_hint ) @overload def mark_processor( func: ProcessorVar, *, weight: float = 0, type_hint: object | None = None, store: str | Store | None = None, ) -> ProcessorVar: ... @overload def mark_processor( func: Literal[None] = ..., *, weight: float = 0, type_hint: object | None = None, store: str | Store | None = None, ) -> Callable[[ProcessorVar], ProcessorVar]: ... @_add_store_to_doc def mark_processor( func: ProcessorVar | None = None, *, weight: float = 0, type_hint: object | None = None, store: str | Store | None = None, ) -> Callable[[ProcessorVar], ProcessorVar] | ProcessorVar: return _store_or_global(store).mark_processor( func, weight=weight, type_hint=type_hint ) @_add_store_to_doc def iter_providers( type_hint: object | type[T], store: str | Store | None = None ) -> Iterable[Callable[[], T | None]]: return _store_or_global(store).iter_providers(type_hint) @_add_store_to_doc def iter_processors( type_hint: object | type[T], store: str | Store | None = None ) -> Iterable[Callable[[T], Any]]: return _store_or_global(store).iter_processors(type_hint) @_add_store_to_doc def provide( type_hint: object | type[T], store: str | Store | None = None, ) -> T | None: return _store_or_global(store).provide(type_hint=type_hint) @_add_store_to_doc def process( result: Any, *, type_hint: object | type[T] | None = None, first_processor_only: bool = False, raise_exception: bool = False, store: str | Store | None = None, ) -> None: return _store_or_global(store).process( result=result, type_hint=type_hint, first_processor_only=first_processor_only, raise_exception=raise_exception, ) @overload def inject( func: Callable[P, R], *, providers: bool = True, processors: bool = False, localns: dict | None = None, on_unresolved_required_args: RaiseWarnReturnIgnore | None = None, on_unannotated_required_args: RaiseWarnReturnIgnore | None = None, guess_self: bool | None = None, store: str | Store | None = None, ) -> Callable[..., R]: ... # unfortunately, the best we can do is convert the signature to Callabe[..., R] # so we lose the parameter information. but it seems better than having # "missing positional args" errors everywhere on injected functions. @overload def inject( func: Literal[None] | None = None, *, providers: bool = True, processors: bool = False, localns: dict | None = None, on_unresolved_required_args: RaiseWarnReturnIgnore | None = None, on_unannotated_required_args: RaiseWarnReturnIgnore | None = None, guess_self: bool | None = None, store: str | Store | None = None, ) -> Callable[[Callable[P, R]], Callable[..., R]]: ... @_add_store_to_doc def inject( func: Callable[P, R] | None = None, *, providers: bool = True, processors: bool = False, localns: dict | None = None, on_unresolved_required_args: RaiseWarnReturnIgnore | None = None, on_unannotated_required_args: RaiseWarnReturnIgnore | None = None, guess_self: bool | None = None, store: str | Store | None = None, ) -> Callable[..., R] | Callable[[Callable[P, R]], Callable[..., R]]: return _store_or_global(store).inject( func=func, providers=providers, processors=processors, localns=localns, on_unresolved_required_args=on_unresolved_required_args, on_unannotated_required_args=on_unannotated_required_args, guess_self=guess_self, ) @overload def inject_processors( func: Callable[P, R], *, hint: object | type[T] | None = None, first_processor_only: bool = False, raise_exception: bool = False, store: str | Store | None = None, ) -> Callable[P, R]: ... @overload def inject_processors( func: Literal[None] | None = None, *, hint: object | type[T] | None = None, first_processor_only: bool = False, raise_exception: bool = False, store: str | Store | None = None, ) -> Callable[[Callable[P, R]], Callable[P, R]]: ... @_add_store_to_doc def inject_processors( func: Callable[P, R] | None = None, *, hint: object | type[T] | None = None, first_processor_only: bool = False, raise_exception: bool = False, store: str | Store | None = None, ) -> Callable[[Callable[P, R]], Callable[P, R]] | Callable[P, R]: return _store_or_global(store).inject_processors( func=func, type_hint=hint, first_processor_only=first_processor_only, raise_exception=raise_exception, ) in_n_out-0.1.8/src/in_n_out/_store.py0000644000000000000000000012322313615410400014523 0ustar00from __future__ import annotations import contextlib import types import warnings import weakref from functools import cached_property, wraps from inspect import CO_VARARGS, isgeneratorfunction, unwrap from logging import getLogger from types import CodeType from typing import ( TYPE_CHECKING, Any, Callable, ClassVar, ContextManager, Iterable, Literal, Mapping, NamedTuple, Optional, Tuple, TypeVar, Union, cast, overload, ) from ._type_resolution import _resolve_sig_or_inform, resolve_type_hints from ._util import _split_union, issubclassable logger = getLogger("in_n_out") if TYPE_CHECKING: from typing_extensions import ParamSpec from ._type_resolution import RaiseWarnReturnIgnore P = ParamSpec("P") R = TypeVar("R") T = TypeVar("T") Provider = Callable[[], Any] # provider should be able to take no arguments Processor = Callable[[Any], Any] # a processor must take one positional arg PPCallback = Union[Provider, Processor] # typevars that retain the signatures of the values passed in ProviderVar = TypeVar("ProviderVar", bound=Provider) ProcessorVar = TypeVar("ProcessorVar", bound=Processor) Disposer = Callable[[], None] Namespace = Mapping[str, object] HintArg = object Weight = float # (callback,) # (callback, type_hint) # (callback, type_hint, weight) ProviderTuple = Union[ Tuple[Provider], Tuple[Provider, HintArg], Tuple[Provider, HintArg, Weight] ] ProcessorTuple = Union[ Tuple[Processor], Tuple[Processor, HintArg], Tuple[Processor, HintArg, Weight] ] CallbackTuple = Union[ProviderTuple, ProcessorTuple] # All of the valid argument that can be passed to register() ProviderIterable = Union[Iterable[ProviderTuple], Mapping[HintArg, Provider]] ProcessorIterable = Union[Iterable[ProcessorTuple], Mapping[HintArg, Processor]] CallbackIterable = Union[ProviderIterable, ProcessorIterable] _GLOBAL = "global" class _NullSentinel: ... class _RegisteredCallback(NamedTuple): origin: type callback: Callable hint_optional: bool weight: float subclassable: bool class _CachedMap(NamedTuple): all: dict[object, list[Processor | Provider]] subclassable: dict[type, list[Processor | Provider]] class InjectionContext(ContextManager): """Context manager for registering callbacks. Primarily used as `with store.regsiter(...)`. """ def __init__( self, store: Store, *, providers: ProviderIterable | None = None, processors: ProcessorIterable | None = None, ) -> None: self._disposers = [] if providers is not None: self._disposers.append(store._register_callbacks(providers, True)) if processors is not None: self._disposers.append(store._register_callbacks(processors, False)) def __exit__(self, *_: Any) -> None: self.cleanup() def cleanup(self) -> Any: """Cleanup any callbacks registered in this context.""" for dispose in self._disposers: dispose() self._disposers.clear() class Store: """A Store is a collection of providers and processors.""" _NULL = _NullSentinel() _instances: ClassVar[dict[str, Store]] = {} @classmethod def create(cls, name: str) -> Store: """Create a new Store instance with the given `name`. This name can be used to refer to the Store in other functions. Parameters ---------- name : str A name for the Store. Returns ------- Store A Store instance with the given `name`. Raises ------ KeyError If the name is already in use, or the name is 'global'. """ name = name.lower() if name == _GLOBAL: raise KeyError("'global' is a reserved store name") elif name in cls._instances: raise KeyError(f"Store {name!r} already exists") cls._instances[name] = cls(name) return cls._instances[name] @classmethod def get_store(cls, name: str | None = None) -> Store: """Get a Store instance with the given `name`. Parameters ---------- name : str The name of the Store. Returns ------- Store A Store instance with the given `name`. Raises ------ KeyError If the name is not in use. """ name = (name or _GLOBAL).lower() if name not in cls._instances: raise KeyError(f"Store {name!r} does not exist") return cls._instances[name] @classmethod def destroy(cls, name: str) -> None: """Destroy Store instance with the given `name`. Parameters ---------- name : str The name of the Store. Raises ------ ValueError If the name matches the global store name. KeyError If the name is not in use. """ name = name.lower() if name == _GLOBAL: raise ValueError("The global store cannot be destroyed") elif name not in cls._instances: raise KeyError(f"Store {name!r} does not exist") del cls._instances[name] def __init__(self, name: str) -> None: self._name = name self._providers: list[_RegisteredCallback] = [] self._processors: list[_RegisteredCallback] = [] self._namespace: Namespace | Callable[[], Namespace] | None = None self.on_unresolved_required_args: RaiseWarnReturnIgnore = "warn" self.on_unannotated_required_args: RaiseWarnReturnIgnore = "warn" self.guess_self: bool = True @property def name(self) -> str: """Return the name of this Store.""" return self._name def clear(self) -> None: """Clear all providers and processors.""" self._providers.clear() self._processors.clear() with contextlib.suppress(AttributeError): del self._cached_processor_map with contextlib.suppress(AttributeError): del self._cached_provider_map @property def namespace(self) -> dict[str, object]: """Return namespace for type resolution, if this store has one. If no namespace is set, this will return an empty `dict`. """ if self._namespace is None: return {} if callable(self._namespace): return dict(self._namespace()) return dict(self._namespace) @namespace.setter def namespace(self, ns: Namespace | Callable[[], Namespace]) -> None: self._namespace = ns # ------------------------- Callback registration ------------------------------ def register( self, *, providers: ProviderIterable | None = None, processors: ProcessorIterable | None = None, ) -> InjectionContext: """Register multiple providers and/or processors at once. This may be used as a context manager to temporarily register providers and/or processors. The format for providers/processors is one of: - a mapping of {type_hint: provider} pairs - an iterable of 1, 2, or 3-tuples, where each tuple in the iterable is: - (callback,) - (callback, type_hint,) - (callback, type_hint, weight) Parameters ---------- providers :Optional[CallbackIterable] mapping or iterable of providers to register. See format in notes above. processors :Optional[CallbackIterable] mapping or iterable of processors to register. See format in notes above. Returns ------- InjectionContext Context manager for unregistering providers and processors. If the context is entered with `with store.register(): ...`, then callbacks will be unregistered when the context is exited. Callbacks may also be unregistered manually using the `.cleanup()` method of the returned context manager. Examples -------- >>> with store.register( providers={int: lambda: 42}, # provided as hint->callback map processors=[ (my_callback), # hint inferred from signature (my_other_callback, str), # hint explicitly provided (my_third_callback, int, 10) # weight explicitly provided ], ): ... """ return InjectionContext(self, providers=providers, processors=processors) def register_provider( self, provider: Provider, type_hint: object | None = None, weight: float = 0, ) -> InjectionContext: """Register `provider` as a provider of `type_hint`. Parameters ---------- provider : Callable A provider callback. Must be able to accept no arguments. type_hint : Optional[object] A type or type hint that `provider` provides. If not provided, it will be inferred from the return annotation of `provider`. weight : float, optional A weight with which to sort this provider. Higher weights are given priority, by default 0 Returns ------- Callable A function that unregisters the provider. """ return self.register(providers=[(provider, type_hint, weight)]) def register_processor( self, processor: Processor, type_hint: object | None = None, weight: float = 0, ) -> InjectionContext: """Register `processor` as a processor of `type_hint`. Processors are callbacks that are invoked when an injected function returns an instance of `type_hint`. Parameters ---------- type_hint : object A type or type hint that `processor` can handle. processor : Callable A processor callback. Must accept at least one argument. weight : float, optional A weight with which to sort this processor. Higher weights are given priority, by default 0. When invoking processors, all processors will be invoked in descending weight order, unless `first_processor_only` is set to `True`. Returns ------- Callable A function that unregisters the processor. """ return self.register(processors=[(processor, type_hint, weight)]) # ------------------------- registration decorators --------------------------- @overload def mark_provider( self, func: ProviderVar, *, type_hint: object | None = None, weight: float = 0, ) -> ProviderVar: ... @overload def mark_provider( self, func: Literal[None] = ..., *, type_hint: object | None = None, weight: float = 0, ) -> Callable[[ProviderVar], ProviderVar]: ... def mark_provider( self, func: ProviderVar | None = None, *, type_hint: object | None = None, weight: float = 0, ) -> Callable[[ProviderVar], ProviderVar] | ProviderVar: """Decorate `func` as a provider of its first parameter type. Note, If func returns `Optional[Type]`, it will be registered as a provider for Type. Parameters ---------- func : Optional[Provider] A function to decorate. If not provided, a decorator is returned. type_hint : Optional[object] Optional type or type hint for which to register this provider. If not provided, the return annotation of `func` will be used. weight : float A weight with which to sort this provider. Higher weights are given priority, by default 0 Returns ------- Union[Callable[[Provider], Provider], Provider] If `func` is not provided, a decorator is returned, if `func` is provided then the function is returned.. Examples -------- >>> @store.provider >>> def provide_int() -> int: ... return 42 """ def _deco(func: ProviderVar) -> ProviderVar: try: self.register_provider(func, type_hint=type_hint, weight=weight) except ValueError as e: warnings.warn(str(e), stacklevel=2) return func return _deco(func) if func is not None else _deco @overload def mark_processor( self, func: ProcessorVar, *, type_hint: object | None = None, weight: float = 0, ) -> ProcessorVar: ... @overload def mark_processor( self, func: Literal[None] = ..., *, type_hint: object | None = None, weight: float = 0, ) -> Callable[[ProcessorVar], ProcessorVar]: ... def mark_processor( self, func: ProcessorVar | None = None, *, type_hint: object | None = None, weight: float = 0, ) -> Callable[[ProcessorVar], ProcessorVar] | ProcessorVar: """Decorate `func` as a processor of its first parameter type. Parameters ---------- func : Optional[Processor], optional A function to decorate. If not provided, a decorator is returned. type_hint : Optional[object] Optional type or type hint that this processor can handle. If not provided, the type hint of the first parameter of `func` will be used. weight : float, optional A weight with which to sort this processor. Higher weights are given priority, by default 0. When invoking processors, all processors will be invoked in descending weight order, unless `first_processor_only` is set to `True`. Returns ------- Union[Callable[[Processor], Processor], Processor] If `func` is not provided, a decorator is returned, if `func` is provided then the function is returned. Examples -------- >>> @store.processor >>> def process_int(x: int) -> None: ... print("Processing int:", x) """ def _deco(func: ProcessorVar) -> ProcessorVar: try: self.register_processor(func, type_hint=type_hint, weight=weight) except ValueError as e: warnings.warn(str(e), stacklevel=2) return func return _deco(func) if func is not None else _deco # ------------------------- Callback retrieval ------------------------------ def iter_providers( self, type_hint: object | type[T] ) -> Iterable[Callable[[], T | None]]: """Iterate over all providers of `type_hint`. Parameters ---------- type_hint : Union[object, Type[T]] A type or type hint for which to return providers. Yields ------ Iterable[Callable[[], Optional[T]]] Iterable of provider callbacks. """ return self._iter_type_map(type_hint, self._cached_provider_map) def iter_processors( self, type_hint: object | type[T] ) -> Iterable[Callable[[T], Any]]: """Iterate over all processors of `type_hint`. Parameters ---------- type_hint : Union[object, Type[T]] A type or type hint for which to return processors. Yields ------ Iterable[Callable[[], Optional[T]]] Iterable of processor callbacks. """ return self._iter_type_map(type_hint, self._cached_processor_map) # ------------------------- Instance retrieval ------------------------------ def provide(self, type_hint: object | type[T]) -> T | None: """Provide an instance of `type_hint`. This will iterate over all providers of `type_hint` and return the first one that returns a non-`None` value. Parameters ---------- type_hint : Union[object, Type[T]] A type or type hint for which to return a value Returns ------- Optional[T] The first non-`None` value returned by a provider, or `None` if no providers return a value. """ for provider in self.iter_providers(type_hint): result = provider() if result is not None: return result return None def process( self, result: Any, *, type_hint: object | type[T] | None = None, first_processor_only: bool = False, raise_exception: bool = False, _funcname: str = "", ) -> None: """Provide an instance of `type_`. This will iterate over all providers of `type_` and invoke the first one that accepts `result`, unless `first_processor_only` is set to `False`, in which case all processors will be invoked. Parameters ---------- result : Any The result to process type_hint : Union[object, Type[T], None], An optional type hint to provide to the processor. If not provided, the type of `result` will be used. first_processor_only : bool, optional If `True`, only the first processor will be invoked, otherwise all processors will be invoked, in descending weight order. raise_exception : bool, optional If `True`, and a processor raises an exception, it will be raised and the remaining processors will not be invoked. _funcname: str, optional The name of the function that called this method. This is used internally for debugging """ if type_hint is None: type_hint = type(result) _processors: Iterable[Callable] = self.iter_processors(type_hint) logger.debug( "Invoking processors on result %r from function %r", result, _funcname ) for processor in _processors: try: logger.debug(" P: %s", processor) processor(result) except Exception as e: # pragma: no cover if raise_exception: raise e warnings.warn( f"Processor {processor!r} failed to process result {result!r}: {e}", stacklevel=2, ) if first_processor_only: break # ----------------------Injection decorators ------------------------------------ @overload def inject( self, func: Callable[P, R], *, providers: bool = True, processors: bool = False, localns: dict | None = None, on_unresolved_required_args: RaiseWarnReturnIgnore | None = None, on_unannotated_required_args: RaiseWarnReturnIgnore | None = None, guess_self: bool | None = None, ) -> Callable[..., R]: ... # unfortunately, the best we can do is convert the signature to Callabe[..., R] # so we lose the parameter information. but it seems better than having # "missing positional args" errors everywhere on injected functions. @overload def inject( self, func: Literal[None] | None = None, *, providers: bool = True, processors: bool = False, localns: dict | None = None, on_unresolved_required_args: RaiseWarnReturnIgnore | None = None, on_unannotated_required_args: RaiseWarnReturnIgnore | None = None, guess_self: bool | None = None, ) -> Callable[[Callable[P, R]], Callable[..., R]]: ... def inject( self, func: Callable[P, R] | None = None, *, providers: bool = True, processors: bool = False, localns: dict | None = None, on_unresolved_required_args: RaiseWarnReturnIgnore | None = None, on_unannotated_required_args: RaiseWarnReturnIgnore | None = None, guess_self: bool | None = None, ) -> Callable[..., R] | Callable[[Callable[P, R]], Callable[..., R]]: """Decorate `func` to inject dependencies at calltime. Assuming `providers` is True (the default), this will attempt retrieve instances of the types required by `func` and inject them into `func` at calltime. The primary consequence of this is that `func` may be called without parameters (assuming the required providers have been registered). See usages examples below. Note that an injected function may *still* be explicitly invoked with parameters. See `register` for more information on how to register providers and processors. Parameters ---------- func : Callable A function to decorate. Type hints are used to determine what to inject. providers : bool Whether to inject dependency providers. If `True` (default), then when this function is called, arguments will be injected into the function call according to providers that have been registered in the store. processors : bool Whether to invoke all processors for this function's return type the when this function is called. Important: this causes *side effects*. By default, `False`. Output processing can also be enabled (with additionl fine tuning) by using the `@store.process_result` decorator. localns : Optional[dict] Optional local namespace for name resolution, by default None on_unresolved_required_args : RaiseWarnReturnIgnore What to do when a required parameter (one without a default) is encountered with an unresolvable type annotation. Must be one of the following (by default 'warn'): - 'raise': immediately raise an exception - 'warn': warn and return the original function - 'return': return the original function without warning - 'ignore': continue decorating without warning (at call time, this function will fail without additional arguments). on_unannotated_required_args : RaiseWarnReturnIgnore What to do when a required parameter (one without a default) is encountered with an *no* type annotation. These functions are likely to fail when called later if the required parameter is not provided. Must be one of the following (by default 'warn'): - 'raise': immediately raise an exception - 'warn': warn, but continue decorating - 'return': immediately return the original function without warning - 'ignore': continue decorating without warning. guess_self : bool Whether to infer the type of the first argument if the function is an unbound class method (by default, `True`) This is done as follows: - if '.' (but not '') is in the function's __qualname__ - and if the first parameter is named 'self' or starts with "_" - and if the first parameter annotation is `inspect.empty` - then the name preceding `func.__name__` in the function's __qualname__ (which is usually the class name), is looked up in the function's `__globals__` namespace. If found, it is used as the first parameter's type annotation. This allows class methods to be injected with instances of the class. Returns ------- Callable A function with dependencies injected Examples -------- >>> import in_n_out as ino >>> class Thing: ... def __init__(self, name: str): ... self.name = name >>> @ino.inject ... def func(thing: Thing): ... return thing.name >>> # no providers available yet >>> func() TypeError: ... missing 1 required positional argument: 'thing' >>> # register a provider >>> ino.register(providers={Thing: Thing('Thing1')}) >>> print(func()) 'Thing1' >>> # can still override with parameters >>> func(Thing('OtherThing')) 'OtherThing' """ on_unres = on_unresolved_required_args or self.on_unresolved_required_args on_unann = on_unannotated_required_args or self.on_unannotated_required_args _guess_self = guess_self or self.guess_self # inner decorator, allows for optional decorator arguments def _inner(func: Callable[P, R]) -> Callable[P, R]: # if the function takes no arguments and has no return annotation # there's nothing to be done if not providers: return self.inject_processors(func) if processors else func # bail if there aren't any annotations at all code: CodeType | None = getattr(unwrap(func), "__code__", None) if (code and not code.co_argcount) and "return" not in getattr( func, "__annotations__", {} ): return func # get a signature object with all type annotations resolved # this may result in a NameError if a required argument is unresolveable. # There may also be unannotated required arguments, which will likely fail # when the function is called later. We break this out into a seperate # function to handle notifying the user on these cases. sig = _resolve_sig_or_inform( func, localns={**self.namespace, **(localns or {})}, on_unresolved_required_args=on_unres, on_unannotated_required_args=on_unann, guess_self=_guess_self, ) if sig is None: # something went wrong, and the user was notified. return func _fname = getattr(func, "__qualname__", func) # get provider functions for each required parameter @wraps(func) def _exec(*args: P.args, **kwargs: P.kwargs) -> R: # we're actually calling the "injected function" now logger.debug( "Executing @injected %s%s with args: %r, kwargs: %r", _fname, sig, args, kwargs, ) # use bind_partial to allow the caller to still provide their own args # if desired. (i.e. the injected deps are only used if not provided) bound = sig.bind_partial(*args, **kwargs) bound.apply_defaults() # first, get and call the provider functions for each parameter type: _injected_names: set[str] = set() for param in sig.parameters.values(): if param.name not in bound.arguments: provided = self.provide(param.annotation) if provided is not None: logger.debug( " injecting %s: %s = %r", param.name, param.annotation, provided, ) _injected_names.add(param.name) bound.arguments[param.name] = provided # call the function with injected values logger.debug( " Calling %s with %r (injected %r)", _fname, bound.arguments, _injected_names, ) try: result = func(**bound.arguments) except TypeError as e: if "missing" not in e.args[0]: raise # pragma: no cover # likely a required argument is still missing. # show what was injected and raise _argnames = ( f"arguments: {_injected_names!r}" if _injected_names else "NO arguments" ) logger.exception(e) for param in sig.parameters.values(): if ( param.name not in bound.arguments and param.default is param.empty ): logger.error( f"Do not have argument for {param.name}: using " "providers " f"{list(self.iter_providers(param.annotation))}" ) raise TypeError( f"After injecting dependencies for {_argnames}, {e}" ) from e return result out = _exec # if it came in as a generatorfunction, it needs to go out as one. if isgeneratorfunction(func): @wraps(func) def _gexec(*args: P.args, **kwargs: P.kwargs) -> R: # type: ignore yield from _exec(*args, **kwargs) # type: ignore out = _gexec # update some metadata on the decorated function. out.__signature__ = sig # type: ignore [attr-defined] out.__annotations__ = { **{p.name: p.annotation for p in sig.parameters.values()}, "return": sig.return_annotation, } out.__doc__ = ( out.__doc__ or "" ) + "\n\n*This function will inject dependencies when called.*" if processors: return self.inject_processors(out, type_hint=sig.return_annotation) return out return _inner(func) if func is not None else _inner @overload def inject_processors( self, func: Callable[P, R], *, type_hint: object | type[T] | None = None, first_processor_only: bool = False, raise_exception: bool = False, ) -> Callable[P, R]: ... @overload def inject_processors( self, func: Literal[None] | None = None, *, type_hint: object | type[T] | None = None, first_processor_only: bool = False, raise_exception: bool = False, ) -> Callable[[Callable[P, R]], Callable[P, R]]: ... def inject_processors( self, func: Callable[P, R] | None = None, *, type_hint: object | type[T] | None = None, first_processor_only: bool = False, raise_exception: bool = False, ) -> Callable[[Callable[P, R]], Callable[P, R]] | Callable[P, R]: """Decorate a function to process its output. Variant of inject, but only injects processors (for the sake of more explicit syntax). When the decorated function is called, the return value will be processed with `store.process(return_value)` before returning the result. Important! This means that calling `func` will likely have *side effects*. Parameters ---------- func : Callable A function to decorate. Return hints are used to determine what to process. type_hint : Union[object, Type[T], None] Type hint for the return value. If not provided, the type will be inferred first from the return annotation of the function, and if that is not provided, from the `type(return_value)`. first_processor_only : bool, optional If `True`, only the first processor will be invoked, otherwise all processors will be invoked, in descending weight order. raise_exception : bool, optional If `True`, and a processor raises an exception, it will be raised and the remaining processors will not be invoked. Returns ------- Callable A function that, when called, will have its return value processed by `store.process(return_value)` """ def _deco(func: Callable[P, R]) -> Callable[P, R]: if isgeneratorfunction(func): raise TypeError( "Cannot decorate a generator function with inject_processors" ) nonlocal type_hint if type_hint is None: annotations = getattr(func, "__annotations__", {}) if "return" in annotations: type_hint = annotations["return"] @wraps(func) def _exec(*args: P.args, **kwargs: P.kwargs) -> R: result = func(*args, **kwargs) if result is not None: self.process( result, type_hint=type_hint, first_processor_only=first_processor_only, raise_exception=raise_exception, _funcname=getattr(func, "__qualname__", str(func)), ) return result return _exec return _deco(func) if func is not None else _deco # ---------------------- Private methods ----------------------- # @cached_property def _cached_provider_map(self) -> _CachedMap: logger.debug("Rebuilding provider map cache") return self._build_map(self._providers) @cached_property def _cached_processor_map(self) -> _CachedMap: logger.debug("Rebuilding processor map cache") return self._build_map(self._processors) def _build_map(self, registry: list[_RegisteredCallback]) -> _CachedMap: """Build a map of type hints to callbacks. This is the sorted and cached version of the map that will be used to resolve a provider or processor. It returns a tuple of two maps. The first is a map of *all* provider/processor type hints, regardless of whether they can be used with `is_subclass`. The second is a map of only "issubclassable" type hints. """ all_: dict[object, list[_RegisteredCallback]] = {} subclassable: dict[type, list[_RegisteredCallback]] = {} for p in registry: if p.origin not in all_: all_[p.origin] = [] all_[p.origin].append(p) if p.subclassable: if p.origin not in subclassable: subclassable[p.origin] = [] subclassable[p.origin].append(p) all_out = { hint: [v.callback for v in sorted(val, key=self._sort_key, reverse=True)] for hint, val in all_.items() } subclassable_out = { hint: [v.callback for v in sorted(val, key=self._sort_key, reverse=True)] for hint, val in subclassable.items() } return _CachedMap(all_out, subclassable_out) def _iter_type_map( self, hint: object | type[T], callback_map: _CachedMap ) -> Iterable[Callable]: _all_types = callback_map.all _subclassable_types = callback_map.subclassable for origin in _split_union(hint)[0]: if origin in _all_types: yield from _all_types[origin] return if isinstance(origin, type): # we need origin to be a type to be able to check if it's a # subclass of other types for _hint, processor in _subclassable_types.items(): if issubclass(origin, _hint): yield from processor return def _sort_key(self, p: _RegisteredCallback) -> float: """How we sort registered callbacks within the same type hint.""" return p.weight def _register_callbacks( self, callbacks: CallbackIterable, is_provider: bool = True, ) -> Disposer: if is_provider: reg = self._providers cache_map = "_cached_provider_map" check_callback: Callable[[Any], Callable] = _validate_provider err_msg = ( "{} has no return type hint (and no hint provided at " "registration). Cannot be a provider." ) def _type_from_hints(hints: dict[str, Any]) -> Any: return hints.get("return") else: reg = self._processors cache_map = "_cached_processor_map" check_callback = _validate_processor err_msg = ( "{} has no argument type hints (and no hint provided " "at registration). Cannot be a processor." ) def _type_from_hints(hints: dict[str, Any]) -> Any: return next((v for k, v in hints.items() if k != "return"), None) _callbacks: Iterable[CallbackTuple] if isinstance(callbacks, Mapping): _callbacks = ((v, k) for k, v in callbacks.items()) # type: ignore # dunno else: _callbacks = callbacks regname = "provider" if is_provider else "processor" to_register: list[_RegisteredCallback] = [] for tup in _callbacks: callback, *rest = tup type_: HintArg | None = None weight: float = 0 if rest: if len(rest) == 1: type_ = rest[0] weight = 0 elif len(rest) == 2: type_, weight = cast(Tuple[Optional[HintArg], float], rest) else: # pragma: no cover raise ValueError(f"Invalid callback tuple: {tup!r}") if type_ is None: hints = resolve_type_hints(callback, localns=self.namespace) type_ = _type_from_hints(hints) if type_ is None: raise ValueError(err_msg.format(callback)) callback = check_callback(callback) if isinstance(callback, types.MethodType): # if the callback is a method, we need to wrap it in a weakref # to prevent a strong reference to the owner object. callback = self._methodwrap(callback, reg, cache_map) origins, is_opt = _split_union(type_) for origin in origins: subclassable: bool = True if not issubclassable(origin): subclassable = False try: hash(origin) except TypeError: raise TypeError( f"{origin!r} cannot be used as a {regname} hint, since it " "is not hashable and cannot be passed as the second " "argument to `issubclass`" ) from None cb = _RegisteredCallback(origin, callback, is_opt, weight, subclassable) logger.debug( "Registering %s of %s: %s (weight: %s, subclassable: %s)", regname, origin, callback, weight, subclassable, ) to_register.append(cb) def _dispose() -> None: for p in to_register: with contextlib.suppress(ValueError): reg.remove(p) logger.debug( "Unregistering %s of %s: %s", regname, p.origin, p.callback ) # attribute error in case the cache was never built with contextlib.suppress(AttributeError): delattr(self, cache_map) if to_register: reg.extend(to_register) # attribute error in case the cache was never built with contextlib.suppress(AttributeError): delattr(self, cache_map) return _dispose def _methodwrap( self, callback: types.MethodType, reg: list[_RegisteredCallback], cache_map: str ) -> Callable: """Wrap a method in a weakref to prevent a strong reference to the owner.""" ref = weakref.WeakMethod(callback) def _callback(*args: Any, **kwargs: Any) -> Any: cb = ref() if cb is not None: return cb(*args, **kwargs) # The callback was garbage collected. Remove it from the registry. for item in reversed(reg): if item.callback is _callback: reg.remove(item) # attribute error in case the cache was never built with contextlib.suppress(AttributeError): delattr(self, cache_map) return _callback def _validate_provider(obj: T | Callable[[], T]) -> Callable[[], T]: """Check that an object is a valid provider. Can either be a function or an object. If a non-callable is passed, a function that returns it is created. """ return obj if callable(obj) else (lambda: cast(T, obj)) def _validate_processor(obj: Callable[[T], Any]) -> Callable[[T], Any]: """Validate a processor. Processors must be a callable that accepts at least one argument(excluding keyword-only arguments). """ if not callable(obj): raise ValueError(f"Processors must be callable. Got {obj!r}") co: CodeType | None = getattr(obj, "__code__", None) if not co: # if we don't have a code object, we can't check the number of arguments # TODO: see if we can do better in the future, but better to just return # the callable for now. return obj if co.co_argcount < 1 and not (co.co_flags & CO_VARARGS): name = getattr(obj, "__name__", None) or obj raise ValueError( f"Processors must take at least one argument. {name!r} takes none." ) return obj # create the global store Store._instances[_GLOBAL] = GLOBAL_STORE = Store(_GLOBAL) in_n_out-0.1.8/src/in_n_out/_type_resolution.py0000644000000000000000000003045313615410400016635 0ustar00from __future__ import annotations import sys import types import typing import warnings from functools import lru_cache, partial from inspect import Signature from typing import TYPE_CHECKING, Any, Callable, ForwardRef try: from toolz import curry PARTIAL_TYPES: tuple[type, ...] = (partial, curry) except ImportError: # pragma: no cover PARTIAL_TYPES = (partial,) if TYPE_CHECKING: from typing import Literal, _get_type_hints_obj_allowed_types RaiseWarnReturnIgnore = Literal["raise", "warn", "return", "ignore"] PY39_OR_GREATER = sys.version_info >= (3, 9) @lru_cache(maxsize=1) def _typing_names() -> dict[str, Any]: return {**typing.__dict__, **types.__dict__} def _unwrap_partial(func: Any) -> Any: while isinstance(func, PARTIAL_TYPES): func = func.func return func def resolve_type_hints( obj: _get_type_hints_obj_allowed_types, globalns: dict | None = None, localns: dict | None = None, include_extras: bool = False, ) -> dict[str, Any]: """Return type hints for an object. This is a small wrapper around `typing.get_type_hints()` that adds namespaces to the global and local namespaces. see docstring for :func:`typing.get_type_hints`. Parameters ---------- obj : module, class, method, or function must be a module, class, method, or function. globalns : Optional[dict] optional global namespace, by default None. localns : Optional[dict] optional local namespace, by default None. include_extras : bool If `False` (the default), recursively replaces all 'Annotated[T, ...]' with 'T'. Returns ------- Dict[str, Any] mapping of object name to type hint for all annotated attributes of `obj`. """ _localns = dict(_typing_names()) if localns: _localns.update(localns) # explicitly provided locals take precedence kwargs: dict[str, Any] = {"globalns": globalns, "localns": _localns} if PY39_OR_GREATER: kwargs["include_extras"] = include_extras return typing.get_type_hints(_unwrap_partial(obj), **kwargs) def resolve_single_type_hints( *objs: Any, localns: dict | None = None, include_extras: bool = False, ) -> tuple[Any, ...]: """Get type hints for one or more isolated type annotations. Wrapper around :func:`resolve_type_hints` (see docstring for that function for parameter docs). `typing.get_type_hints()` only works for modules, classes, methods, or functions, but the typing module doesn't make the underlying type evaluation logic publicly available. This function creates a small mock object with an `__annotations__` dict that will work as an argument to `typing.get_type_hints()`. It then extracts the resolved hints back into a tuple of hints corresponding to the input objects. Returns ------- Tuple[Any, ...] Tuple >>> resolve_single_type_hints('hi', localns={'hi': typing.Any}) (typing.Any,) """ annotations = {str(n): v for n, v in enumerate(objs)} mock_obj = type("_T", (), {"__annotations__": annotations})() hints = resolve_type_hints(mock_obj, localns=localns, include_extras=include_extras) return tuple(hints[k] for k in annotations) def type_resolved_signature( func: Callable, *, localns: dict | None = None, raise_unresolved_optional_args: bool = True, raise_unresolved_required_args: bool = True, guess_self: bool = True, ) -> Signature: """Return a Signature object for a function with resolved type annotations. Parameters ---------- func : Callable A callable object. localns : Optional[dict] Optional local namespace for name resolution, by default None raise_unresolved_optional_args : bool Whether to raise an exception when an optional parameter (one with a default value) has an unresolvable type annotation, by default True raise_unresolved_required_args : bool Whether to raise an exception when a required parameter has an unresolvable type annotation, by default True guess_self : bool Whether to infer the type of the first argument if the function is an unbound class method. This is done as follows: - if '.' (but not '') is in the function's __qualname__ - and if the first parameter is named 'self' or starts with "_" - and if the first parameter annotation is `inspect.empty` - then the name preceding `func.__name__` in the function's __qualname__ (which is usually the class name), is looked up in the function's `__globals__` namespace. If found, it is used as the first parameter's type annotation. This allows class methods to be injected with instances of the class. Returns ------- Signature :class:`inspect.Signature` object with fully resolved type annotations, (or at least partially resolved type annotations if `raise_unresolved_optional_args` is `False`). Raises ------ NameError If a required argument has an unresolvable type annotation, or if `raise_unresolved_optional_args` is `True` and an optional argument has an unresolvable type annotation. """ sig = Signature.from_callable(func) hints = {} if guess_self and sig.parameters: p0 = next(iter(sig.parameters.values())) # The best identifier i can figure for a class method is that: # 1. its qualname contains a period (e.g. "MyClass.my_method"), # 2. the first parameter tends to be named "self", or some private variable # 3. the first parameter tends to be unannotated qualname = getattr(func, "__qualname__", "") if ( "." in qualname and "" not in qualname # don't support locally defd types and (p0.name == "self" or p0.name.startswith("_")) and p0.annotation is p0.empty ): # look up the class name in the function's globals cls_name = qualname.replace(func.__name__, "").rstrip(".") func_globals = getattr(func, "__globals__", {}) if cls_name in func_globals: # add it to the type hints hints = {p0.name: func_globals[cls_name]} try: hints.update(resolve_type_hints(func, localns=localns)) except (NameError, TypeError) as err: if raise_unresolved_optional_args: raise NameError( f"Could not resolve all annotations in signature {sig} ({err}). " "To allow optional parameters and return types to remain unresolved, " "use `raise_unresolved_optional_args=False`" ) from err hints = _resolve_params_one_by_one( sig, localns=localns, exclude_unresolved_mandatory=not raise_unresolved_required_args, ) resolved_parameters = [ param.replace(annotation=hints.get(param.name, param.empty)) for param in sig.parameters.values() ] return sig.replace( parameters=resolved_parameters, return_annotation=hints.get("return", sig.empty), ) def _resolve_params_one_by_one( sig: Signature, localns: dict | None = None, exclude_unresolved_optionals: bool = False, exclude_unresolved_mandatory: bool = False, ) -> dict[str, Any]: """Resolve all required param annotations in `sig`, but allow optional ones to fail. Helper function for :func:`type_resolved_signature`. This fallback function is used if at least one parameter in `sig` has an unresolvable type annotation. It resolves each parameter's type annotation independently, and only raises an error if a parameter without a default value has an unresolvable type annotation. Parameters ---------- sig : Signature :class:`inspect.Signature` object with unresolved type annotations. localns : Optional[dict] Optional local namespace for name resolution, by default None exclude_unresolved_optionals : bool Whether to exclude parameters with unresolved type annotations that have a default value, by default False exclude_unresolved_mandatory : bool Whether to exclude parameters with unresolved type annotations that do not have a default value, by default False Returns ------- Dict[str, Any] mapping of parameter name to type hint. Raises ------ NameError If a required argument has an unresolvable type annotation. """ hints = {} for name, param in sig.parameters.items(): if param.annotation is sig.empty: continue # pragma: no cover try: hints[name] = resolve_single_type_hints(param.annotation, localns=localns)[ 0 ] except NameError as e: if ( param.default is param.empty and exclude_unresolved_mandatory or param.default is not param.empty and not exclude_unresolved_optionals ): hints[name] = param.annotation elif param.default is param.empty: raise NameError( f"Could not resolve type hint for required parameter {name!r}: {e}" ) from e if sig.return_annotation is not sig.empty: try: hints["return"] = resolve_single_type_hints( sig.return_annotation, localns=localns )[0] except NameError: if not exclude_unresolved_optionals: hints["return"] = sig.return_annotation return hints def _resolve_sig_or_inform( func: Callable, localns: dict | None, on_unresolved_required_args: RaiseWarnReturnIgnore, on_unannotated_required_args: RaiseWarnReturnIgnore, guess_self: bool = True, ) -> Signature | None: """Return a resolved signature, or None if the function should be returned as-is. Helper function for user warnings/errors during inject_dependencies. all parameters are described above in inject_dependencies """ sig = type_resolved_signature( func, localns=localns, raise_unresolved_optional_args=False, raise_unresolved_required_args=False, guess_self=guess_self, ) for param in sig.parameters.values(): if param.default is not param.empty: continue # pragma: no cover if isinstance(param.annotation, (str, ForwardRef)): errmsg = ( f"Could not resolve type hint for required parameter {param.name!r}" ) if on_unresolved_required_args == "raise": msg = ( f"{errmsg}. To simply return the original function, pass `on_un" 'annotated_required_args="return"`. To emit a warning, pass "warn".' ) raise NameError(msg) elif on_unresolved_required_args == "warn": msg = ( f"{errmsg}. To suppress this warning and simply return the original" ' function, pass `on_unannotated_required_args="return"`.' ) warnings.warn(msg, UserWarning, stacklevel=2) elif on_unresolved_required_args == "return": return None elif param.annotation is param.empty: fname = (getattr(func, "__name__", ""),) name = param.name base = ( f"Injecting dependencies on function {fname!r} with a required, " f"unannotated parameter {name!r}. This will fail later unless that " "parameter is provided at call-time.", ) if on_unannotated_required_args == "raise": msg = ( f'{base} To allow this, pass `on_unannotated_required_args="ignore"' '`. To emit a warning, pass "warn".' ) raise TypeError(msg) elif on_unannotated_required_args == "warn": msg = ( f'{base} To allow this, pass `on_unannotated_required_args="ignore"' '`. To raise an exception, pass "raise".' ) warnings.warn(msg, UserWarning, stacklevel=2) elif on_unannotated_required_args == "return": return None return sig in_n_out-0.1.8/src/in_n_out/_util.py0000644000000000000000000000172213615410400014343 0ustar00import types from typing import Any, List, Set, Tuple, Type, Union, cast, get_origin _compiled: bool = False UNION_TYPES: Set[Any] = {Union} if hasattr(types, "UnionType"): # doing it this way to deal with python-version specific linting issues UNION_TYPES.add(cast(Any, getattr(types, "UnionType"))) # noqa def _is_union(type_: Any) -> bool: return get_origin(type_) in UNION_TYPES def _split_union(type_: Any) -> Tuple[List[Type], bool]: optional = False if _is_union(type_): types = [] for arg in getattr(type_, "__args__", ()): if arg is type(None): optional = True else: types.append(arg) else: types = [type_] return types, optional def issubclassable(obj: Any) -> bool: """Return True if `obj` can be used as the second argument in issubclass.""" try: issubclass(type, obj) return True except TypeError: return False in_n_out-0.1.8/src/in_n_out/py.typed0000644000000000000000000000000013615410400014340 0ustar00in_n_out-0.1.8/tests/__init__.py0000644000000000000000000000000013615410400013513 0ustar00in_n_out-0.1.8/tests/conftest.py0000644000000000000000000000047613615410400013622 0ustar00from typing import Iterator import pytest from in_n_out._store import Store @pytest.fixture def test_store() -> Iterator[Store]: try: yield Store.create("test") finally: Store.destroy("test") @pytest.fixture(autouse=True) def clear_global_store(): yield Store.get_store().clear() in_n_out-0.1.8/tests/test_benchmarks.py0000644000000000000000000000245613615410400015151 0ustar00from __future__ import annotations import sys from typing import Callable import pytest import in_n_out as ino if all(x not in {"--codspeed", "--benchmark", "tests/test_bench.py"} for x in sys.argv): pytest.skip("use --benchmark to run benchmark", allow_module_level=True) def some_func(x: int, y: str) -> tuple[int, str]: return x, y def returns_str(x: int, y: str) -> str: return str(x) + y def test_time_to_inject(benchmark: Callable) -> None: benchmark(ino.inject, some_func) def test_time_run_injected_no_injections(benchmark: Callable) -> None: injected = ino.inject(some_func) benchmark(injected, 1, "a") def test_time_run_injected_2_injections(benchmark: Callable) -> None: injected = ino.inject(some_func) with ino.register(providers=[(lambda: 1, int), (lambda: "a", str)]): benchmark(injected) def test_time_run_process(benchmark: Callable) -> None: injected = ino.inject_processors(returns_str) with ino.register(processors=[(lambda x: print(x), str)]): benchmark(injected, 1, "hi") def test_time_run_inject_and_process(benchmark: Callable) -> None: injected = ino.inject(returns_str, processors=True) with ino.register( providers=[(lambda: "a", str)], processors=[(lambda x: ..., str)] ): benchmark(injected, 1) in_n_out-0.1.8/tests/test_injection.py0000644000000000000000000001654013615410400015015 0ustar00import functools from contextlib import nullcontext from inspect import isgeneratorfunction from typing import ContextManager, Generator, Optional from unittest.mock import Mock import pytest from in_n_out import Store, _compiled, inject, inject_processors, register def test_injection(): @inject def f(i: int, s: str): return (i, s) with register(providers={int: lambda: 1, str: lambda: "hi"}): assert f() == (1, "hi") @pytest.mark.parametrize("order", ["together", "inject_first", "inject_last"]) def test_inject_deps_and_providers(order): mock = Mock() mock2 = Mock() def f(i: int) -> str: mock(i) return str(i) if order == "together": f = inject(f, providers=True, processors=True) elif order == "inject_first": f = inject_processors(inject(f)) elif order == "inject_last": f = inject(inject_processors(f)) with register(providers={int: lambda: 1}, processors={str: mock2}): assert f() == "1" mock.assert_called_once_with(1) mock2.assert_called_once_with("1") def test_inject_only_providers(): mock = Mock() def f(i: int) -> str: mock(i) return str(i) f2 = inject(f, providers=False, processors=False) assert f2 is f f3 = inject(f, providers=False, processors=True) assert f3 is not f2 with register(processors={str: mock}): assert f(1) == "1" mock.assert_called_once_with(1) def test_injection_missing(): @inject def f(x: int): return x with pytest.raises(TypeError, match="After injecting dependencies"): f() assert f(4) == 4 with register(providers={int: lambda: 1}): assert f() == 1 def test_set_processor(): @inject_processors def f2(x: int) -> int: return x # calling mock inside process_int to preserve the __code__ object # on the processor function mock = Mock() def process_int(x: int) -> None: mock(x) with register(processors={int: process_int}): assert f2(3) == 3 mock.assert_called_once_with(3) def test_injection_with_generator(): @inject def f(x: int): yield x # setting the accessor to our local viewer with register(providers={int: lambda: 1}): assert tuple(f()) == (1,) def test_injection_without_args(): """it just returns the same function""" def f(): ... assert inject(f) is f modes = ["raise", "warn", "return", "ignore"] def unannotated(x) -> int: # type: ignore ... def unknown(v: "Unknown") -> int: # type: ignore # noqa ... def unknown_and_unannotated(v: "Unknown", x) -> int: # type: ignore # noqa ... @pytest.mark.parametrize("on_unresolved", modes) @pytest.mark.parametrize("on_unannotated", modes) @pytest.mark.parametrize("in_func", [unknown, unannotated, unknown_and_unannotated]) def test_injection_errors(in_func, on_unresolved, on_unannotated): ctx: ContextManager = nullcontext() ctxb: ContextManager = nullcontext() expect_same_func_back = False UNANNOTATED_MSG = "Injecting dependencies .* with a required, unannotated param" if "unknown" in in_func.__name__ and on_unresolved != "ignore": # required params with unknown annotations UNRESOLVED_MSG = "Could not resolve type hint for required parameter" if on_unresolved == "raise": ctx = pytest.raises(NameError, match=UNRESOLVED_MSG) elif on_unresolved == "warn": ctx = pytest.warns(UserWarning, match=UNRESOLVED_MSG) if "unannotated" in in_func.__name__: if on_unannotated == "raise": ctxb = pytest.raises(TypeError, match=UNANNOTATED_MSG) elif on_unannotated == "return": expect_same_func_back = True else: expect_same_func_back = True elif "unannotated" in in_func.__name__: # required params without annotations if on_unannotated == "raise": ctx = pytest.raises(TypeError, match=UNANNOTATED_MSG) elif on_unannotated == "warn": ctx = pytest.warns(UserWarning, match=UNANNOTATED_MSG) elif on_unannotated == "return": expect_same_func_back = True with ctx, ctxb: out_func = inject( in_func, on_unannotated_required_args=on_unannotated, on_unresolved_required_args=on_unresolved, ) assert (out_func is in_func) is expect_same_func_back def test_processors_not_passed_none(test_store: Store): @test_store.inject_processors def f(x: int) -> Optional[int]: return x if x > 5 else None mock = Mock() # regardless of whether a process accepts "Optional" or not, # we won't call it unless the value is not None # i.e. this could also be `(x: Optional[int])` and it would only be called with int def process_int(x: int) -> None: mock(x) with test_store.register(processors={int: process_int}): assert f(3) is None mock.assert_not_called() assert f(10) == 10 mock.assert_called_once_with(10) def test_optional_provider_with_required_arg(test_store: Store): mock = Mock() @inject(store=test_store) def f(x: int): mock(x) with test_store.register(providers={Optional[int]: lambda: None}): with pytest.raises(TypeError, match="missing 1 required positional argument"): f() mock.assert_not_called() with test_store.register(providers={Optional[int]: lambda: 2}): f() mock.assert_called_once_with(2) class Foo: def method(self): return self def test_inject_instance_into_unbound_method(): foo = Foo() with register(providers={Foo: lambda: foo}): assert inject(Foo.method)() == foo # https://github.com/cython/cython/issues/4888 @pytest.mark.xfail(bool(_compiled), reason="Cython doesn't support this") def test_generators(): def generator_func() -> Generator: yield 1 yield 2 yield 3 assert isgeneratorfunction(generator_func) assert list(generator_func()) == [1, 2, 3] injected = inject(generator_func) assert isgeneratorfunction(injected) assert list(injected()) == [1, 2, 3] with pytest.raises(TypeError, match="generator function"): inject(generator_func, processors=True) def test_wrapped_functions(): def func(foo: Foo): return foo @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) @functools.wraps(wrapper) def wrapper2(*args, **kwargs): return wrapper(*args, **kwargs) injected = inject(wrapper2) foo = Foo() with register(providers={Foo: lambda: foo}): assert injected() == foo def test_partial_annotations(test_store: Store): def func(foo: "Foo", bar: "Bar"): # noqa return foo, bar # other way around def func2(bar: "Bar", foo: "Foo"): # noqa return foo, bar with pytest.warns(UserWarning): injected = test_store.inject(func) test_store.namespace = {"Foo": Foo} injected = test_store.inject(func, on_unresolved_required_args="ignore") injected2 = test_store.inject(func2, on_unresolved_required_args="ignore") foo = Foo() with test_store.register(providers={Foo: lambda: foo}): assert injected(bar=2) == (foo, 2) # type: ignore assert injected2(2) == (foo, 2) # type: ignore in_n_out-0.1.8/tests/test_logging.py0000644000000000000000000000326613615410400014462 0ustar00import sys from typing import TYPE_CHECKING import pytest import in_n_out as ino if TYPE_CHECKING: from pytest import LogCaptureFixture @pytest.mark.skipif(sys.version_info < (3, 9), reason="output differs on 3.8") def test_logging(caplog: "LogCaptureFixture") -> None: caplog.set_level("DEBUG") VAL = "hi" def p1() -> str: return VAL def proc(x: str) -> None: ... ctx_a = ino.register(providers=[(p1, str)]) assert caplog.records[0].message.startswith( "Registering provider of : .p1" ) ctx_b = ino.register(processors=[(proc, str)]) assert caplog.records[1].message.startswith( "Registering processor of : .proc" ) @ino.inject(processors=True) def f(x: str) -> str: return x f() assert [r.message[:50] for r in caplog.records[2:-1]] == [ "Executing @injected test_logging..f(x: str", "Rebuilding provider map cache", f" injecting x: = '{VAL}'", f" Calling test_logging..f with {{'x': '{VAL}'}}", "Rebuilding processor map cache", f"Invoking processors on result '{VAL}' from function '", ] assert caplog.records[-1].message.startswith( " P: .proc at 0" ) ctx_a.cleanup() assert caplog.records[-1].message.startswith( "Unregistering provider of : .p1" ) ctx_b.cleanup() assert caplog.records[-1].message.startswith( "Unregistering processor of : .proc" ) in_n_out-0.1.8/tests/test_processors.py0000644000000000000000000001142713615410400015234 0ustar00from typing import Optional, Sequence, Union from unittest.mock import Mock import pytest import in_n_out as ino R = object() MOCK = Mock() @pytest.mark.parametrize( "type, process, ask_type", [ (int, lambda x: MOCK(), int), # processor can be a function # we can ask for a subclass of a provided types (Sequence, lambda x: MOCK(), list), (Union[list, tuple], lambda x: MOCK(), tuple), (Union[list, tuple], lambda x: MOCK(), list), ], ) def test_set_processors(type, process, ask_type): """Test that we can set processor as function or constant, and get it back.""" assert not list(ino.iter_processors(ask_type)) with ino.register(processors={type: process}): assert list(ino.iter_processors(type)) assert list(ino.iter_processors(ask_type)) MOCK.reset_mock() ino.process(1, type_hint=ask_type) MOCK.assert_called_once() # make sure context manager cleaned up assert not list(ino.iter_processors(ask_type)) def test_set_processors_cleanup(test_store: ino.Store): """Test that we can set processors in contexts, and cleanup""" assert not list(test_store.iter_processors(int)) mock = Mock() mock2 = Mock() with test_store.register(processors={int: lambda v: mock(v)}): assert len(test_store._processors) == 1 test_store.process(2) mock.assert_called_once_with(2) mock.reset_mock() with test_store.register(processors=[(lambda x: mock2(x * x), int, 10)]): assert len(test_store._processors) == 2 test_store.process(2, first_processor_only=True) mock2.assert_called_once_with(4) mock.assert_not_called() mock2.reset_mock() assert len(test_store._processors) == 1 test_store.process(2) mock.assert_called_once_with(2) mock2.assert_not_called() assert not list(test_store.iter_processors(int)) def test_processor_decorator(test_store: ino.Store): """Test the @processor decorator.""" assert not list(test_store.iter_processors(int)) @test_store.mark_processor def processes_int(x: int): ... assert next(test_store.iter_processors(int)) is processes_int test_store.clear() assert not list(test_store.iter_processors(int)) def test_optional_processors(test_store: ino.Store): """Test processing Optional[type].""" assert not list(test_store.iter_processors(Optional[int])) assert not list(test_store.iter_processors(str)) @test_store.mark_processor def processes_int(x: int): return 1 @test_store.mark_processor # these decorators are equivalent def processes_string(x: str): ... # we don't have a processor guaranteed to take an int # assert not get_processor(int) # just an optional one assert next(test_store.iter_processors(Optional[int])) is processes_int # but processes_string takes a string assert next(test_store.iter_processors(str)) is processes_string # which means it also provides an Optional[str] assert next(test_store.iter_processors(Optional[str])) is processes_string assert next(test_store.iter_processors(int)) is processes_int # the definite processor takes precedence # TODO: consider this... assert next(test_store.iter_processors(Optional[int])) is processes_int def test_union_processors(test_store: ino.Store): @test_store.mark_processor def processes_int_or_str(x: Union[int, str]): return 1 assert next(test_store.iter_processors(int)) is processes_int_or_str assert next(test_store.iter_processors(str)) is processes_int_or_str def test_unlikely_processor(): with pytest.warns(UserWarning, match="has no argument type hints"): @ino.mark_processor def provides_int(): ... with pytest.raises(ValueError, match="Processors must take at least one argument"): ino.register(processors={int: lambda: 1}) with pytest.raises(ValueError, match="Processors must be callable"): ino.register(processors={int: 1}) def test_global_register(): mock = Mock() def f(x: int): mock(x) ino.register_processor(f) ino.process(1) mock.assert_called_once_with(1) def test_processor_provider_recursion() -> None: """Make sure to avoid infinte recursion when a provider uses processors.""" class Thing: count = 0 # this is both a processor and a provider @ino.register_provider @ino.inject_processors def thing_provider() -> Thing: return Thing() @ino.inject def add_item(thing: Thing) -> None: thing.count += 1 N = 3 for _ in range(N): ino.register_processor(add_item) @ino.inject def func(thing: Thing) -> int: return thing.count assert func() == N in_n_out-0.1.8/tests/test_providers.py0000644000000000000000000000575713615410400015060 0ustar00from typing import Optional, Sequence import pytest import in_n_out as ino def test_provider_resolution(): with ino.register( providers=[ (lambda: None, Optional[int]), (lambda: 2, Optional[int]), (lambda: 1, int), ] ): assert ino.Store.get_store().provide(Optional[int]) == 2 @pytest.mark.parametrize( "type, provider, ask_type, expect", [ (int, lambda: 1, int, 1), # provider can be a function (int, 1, int, 1), # or a constant value (Sequence, [], list, []), # we can ask for a subclass of a provided types ], ) def test_register_providers(type, provider, ask_type, expect): """Test that we can set provider as either function or constant, and get it back.""" assert not ino.provide(ask_type) with ino.register_provider(provider=provider, type_hint=type): assert ino.provide(ask_type) == expect assert not ino.provide(ask_type) # make sure context manager cleaned up def test_provider_decorator(test_store: ino.Store): """Test the @provider decorator.""" assert not test_store.provide(int) @test_store.mark_provider def provides_int() -> int: return 1 assert next(ino.iter_providers(int, store=test_store)) is provides_int assert test_store.provide(int) == 1 test_store.clear() assert not test_store.provide(int) def test_optional_providers(test_store: ino.Store): """Test providing & getting Optional[type].""" assert not list(test_store.iter_providers(Optional[int])) assert not list(test_store.iter_providers(str)) @test_store.mark_provider def provides_optional_int() -> Optional[int]: return 1 @test_store.mark_provider def provides_str() -> str: return "hi" assert test_store.provide(int) == 1 # just an optional one assert next(test_store.iter_providers(Optional[int])) is provides_optional_int # but provides_str returns a string assert next(test_store.iter_providers(str)) is provides_str # which means it also provides an Optional[str] assert next(test_store.iter_providers(Optional[str])) is provides_str # also register a provider for int @test_store.mark_provider(weight=10) def provides_int() -> int: return 1 assert next(test_store.iter_providers(int)) is provides_int # the definite provider takes precedence # TODO: consider this... assert next(test_store.iter_providers(Optional[int])) is provides_int test_store.clear() # all clear assert not test_store._processors assert not test_store._providers def test_unlikely_provider(): with pytest.warns(UserWarning, match="has no return type hint"): @ino.mark_provider def provides_int(): ... with pytest.raises(ValueError, match="has no return type hint"): ino.register_provider(lambda: None) def test_global_register(): def f() -> int: return 1 ino.register_provider(f) assert ino.provide(int) == 1 in_n_out-0.1.8/tests/test_store.py0000644000000000000000000000546313615410400014171 0ustar00from typing import Optional from unittest.mock import Mock import pytest from in_n_out import Store, inject, mark_provider from in_n_out._store import _GLOBAL def test_create_get_destroy(): assert len(Store._instances) == 1 assert Store.get_store().name == _GLOBAL name = "test" test_store = Store.create(name) assert test_store is Store.get_store(name) assert len(Store._instances) == 2 with pytest.raises(KeyError, match=f"Store {name!r} already exists"): Store.create(name) Store.destroy(name) assert len(Store._instances) == 1 with pytest.raises(KeyError, match=f"Store {name!r} does not exist"): Store.get_store(name) with pytest.raises(KeyError, match=f"Store {name!r} does not exist"): Store.destroy(name) with pytest.raises(ValueError, match="The global store cannot be destroyed"): Store.destroy(_GLOBAL) with pytest.raises(KeyError, match=f"{_GLOBAL!r} is a reserved store name"): Store.create(_GLOBAL) assert len(Store._instances) == 1 def test_store_clear(test_store: Store): assert not test_store._providers assert not test_store._processors test_store.register(providers={int: 1}) test_store.register(providers={Optional[str]: None}) test_store.register(processors={int: print}) assert len(test_store._providers) == 2 assert len(test_store._processors) == 1 test_store.clear() assert not test_store._providers assert not test_store._processors def test_store_namespace(test_store: Store): class T: ... @mark_provider(store=test_store) def provide_t() -> T: return T() # namespace can be a static dict test_store.namespace = {"Hint": T} @inject(store=test_store) def use_t(t: "Hint") -> None: # type: ignore # noqa: F821 return t assert isinstance(use_t(), T) # namespace can also be a callable test_store.namespace = lambda: {"Hint2": T} @inject(store="test") def use_t2(t: "Hint2") -> None: # type: ignore # noqa: F821 return t assert isinstance(use_t2(), T) def test_weakrefs_to_bound_methods(test_store: Store): import gc import weakref mock = Mock() class T: def func(self, foo: int) -> None: mock(foo) def func2(self) -> str: return "hi" t = T() reft = weakref.ref(t) test_store.register_processor(t.func) test_store.process(1) mock.assert_called_once_with(1) mock.reset_mock() test_store.register_provider(t.func2) assert test_store.provide(str) == "hi" del t gc.collect() gc.collect() assert reft() is None test_store.process(1) mock.assert_not_called() assert test_store.provide(str) is None assert not test_store._providers assert not test_store._processors in_n_out-0.1.8/tests/test_type_resolution.py0000644000000000000000000000732213615410400016275 0ustar00import types from typing import Any, Callable, Optional import pytest from in_n_out import ( resolve_single_type_hints, resolve_type_hints, type_resolved_signature, ) from in_n_out._type_resolution import _resolve_sig_or_inform def basic_sig(a: "int", b: "str", c: "Optional[float]" = None) -> int: ... def requires_unknown(param: "Unknown", x) -> "Unknown": # type: ignore # noqa ... def optional_unknown(param: "Unknown" = 1) -> "Unknown": # type: ignore # noqa ... def test_resolve_type_hints(): with pytest.raises(NameError): resolve_type_hints(requires_unknown) hints = resolve_type_hints(basic_sig) assert hints["c"] == Optional[float] hints = resolve_type_hints(requires_unknown, localns={"Unknown": int}) assert hints["param"] == int def test_resolve_single_type_hints(): hints = resolve_single_type_hints( int, "Optional[int]", "FunctionType", "Callable[..., Any]", ) assert hints == ( int, Optional[int], types.FunctionType, Callable[..., Any], ) def test_type_resolved_signature(): sig = type_resolved_signature(basic_sig) assert sig.parameters["c"].annotation == Optional[float] with pytest.raises(NameError, match="use `raise_unresolved_optional_args=False`"): type_resolved_signature(optional_unknown) sig = type_resolved_signature( optional_unknown, raise_unresolved_optional_args=False ) assert sig.parameters["param"].annotation == "Unknown" with pytest.raises(NameError, match="Could not resolve all annotations"): type_resolved_signature(requires_unknown) with pytest.raises( NameError, match="Could not resolve type hint for required parameter 'param'", ): type_resolved_signature(requires_unknown, raise_unresolved_optional_args=False) sig = type_resolved_signature(requires_unknown, localns={"Unknown": int}) assert sig.parameters["param"].annotation == int def test_partial_resolution() -> None: from functools import partial def func(x: int, y: str, z: list): ... pf = partial(func, 1) ppf = partial(pf, z=["hi"]) assert resolve_type_hints(ppf) == {"x": int, "y": str, "z": list} def test_curry_resolution() -> None: toolz = pytest.importorskip("toolz") @toolz.curry def func2(x: int, y: str, z: list): ... pf = func2(x=1) ppf = pf(z=["hi"]) assert resolve_type_hints(ppf) == {"x": int, "y": str, "z": list} def test_wrapped_resolution() -> None: from functools import wraps def func(x: int, y: str, z: list): ... @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) @wraps(wrapper) def wrapper2(*args, **kwargs): return wrapper(*args, **kwargs) assert resolve_type_hints(wrapper2) == {"x": int, "y": str, "z": list} def test_resolve_sig_or_inform(): """Make sure we can partially resolve annotations.""" class Foo: ... def func(foo: "Foo", bar: "Bar"): # noqa return foo, bar sig = _resolve_sig_or_inform( func, localns={"Foo": Foo}, on_unresolved_required_args="ignore", on_unannotated_required_args="ignore", ) assert sig.parameters["foo"].annotation == Foo assert sig.parameters["bar"].annotation == "Bar" # other way around def func2(bar: "Bar", foo: "Foo"): # noqa return foo, bar sig2 = _resolve_sig_or_inform( func2, localns={"Foo": Foo}, on_unresolved_required_args="ignore", on_unannotated_required_args="ignore", ) assert sig2.parameters["foo"].annotation == Foo assert sig2.parameters["bar"].annotation == "Bar" in_n_out-0.1.8/tests/test_type_support.py0000644000000000000000000000436413615410400015611 0ustar00from collections import ChainMap from typing import ( TYPE_CHECKING, Callable, Generic, Iterable, List, Mapping, MutableMapping, NewType, Sequence, Set, TypeVar, ) from unittest.mock import Mock import pytest if TYPE_CHECKING: from in_n_out import Store T = TypeVar("T") class G(Generic[T]): ... nt = NewType("nt", int) NON_SUBCLASSABLE_TYPES = ["hi", nt, List[nt], T, Callable[[int], str], G[int]] SUBCLASS_PAIRS = [ (list, Sequence), (tuple, Sequence), (dict, Mapping), (set, Set), (list, Iterable), (ChainMap, MutableMapping), ] @pytest.mark.parametrize("type_", NON_SUBCLASSABLE_TYPES) @pytest.mark.parametrize("mode", ["provider", "processor"]) def test_non_standard_types(test_store: "Store", type_, mode) -> None: mock = Mock(return_value=1) if mode == "provider": test_store.register_provider(mock, type_) assert test_store.provide(type_) == 1 mock.assert_called_once() else: test_store.register_processor(mock, type_) test_store.process(2, type_hint=type_) mock.assert_called_once_with(2) def test_provider_type_error(test_store: "Store") -> None: with pytest.raises(TypeError, match="cannot be used as a provider hint"): test_store.register_provider(lambda: 1, set()) with pytest.raises(TypeError, match="cannot be used as a processor hint"): test_store.register_processor(lambda x: None, set()) @pytest.mark.parametrize("sub, sup", SUBCLASS_PAIRS) @pytest.mark.parametrize("mode", ["provider", "processor"]) def test_subclass_pairs(test_store: "Store", sub, sup, mode) -> None: mock = Mock(return_value=1) if mode == "provider": test_store.register_provider(mock, sup) assert test_store.provide(sub) == 1 mock.assert_called_once() else: test_store.register_processor(mock, sup) test_store.process(2, type_hint=sub) mock.assert_called_once_with(2) test_store.clear() mock.reset_mock() if mode == "provider": test_store.register_provider(sub, mock) assert test_store.provide(sup) is None else: test_store.register_processor(sub, mock) test_store.process(2, type_hint=sup) mock.assert_not_called() in_n_out-0.1.8/tests/typesafety/__init__.py0000644000000000000000000000023613615410400015723 0ustar00import pytest pytest.skip("flaky pytest mypy plugins", allow_module_level=True) # FIXME: re-enable after looking into testing with pytest-mypy-plugins again in_n_out-0.1.8/tests/typesafety/test_types.py0000644000000000000000000001046413615410400016373 0ustar00""" these tests aren't actually executed. They are passed to mypy. Use the following "assertion" comments # N: - we expect a mypy note message # W: - we expect a mypy warning message # E: - we expect a mypy error message # R: - we expect a mypy note message Revealed type is '' """ import pytest import in_n_out as ino # flake8: noqa # fmt: off @pytest.mark.mypy_testing def mypy_test_injection() -> None: def f(x: int) -> int: raise ValueError() f() # E: Missing positional argument "x" in call to "f" [call-arg] deco = ino.inject(providers=True, processors=True) reveal_type(deco) # R: def [P, R] (def (*P.args, **P.kwargs) -> R`-2) -> def (*Any, **Any) -> R`-2 injected = deco(f) reveal_type(injected) # R: def (*Any, **Any) -> builtins.int injected() # no error injected(1) # unfortunately, the best we can do is convert the signature to Callabe[..., R] # so we lose the parameter information. but it seems better than having # "missing positional args" errors everywhere on injected functions. injected(1, 2) @pytest.mark.mypy_testing def mypy_test_provider() -> None: store = ino.Store('name') def func1(x: str) -> int: ... # func1 requires an argument, and so cannot be a provider outfunc1 = ino.mark_provider(func1) # E: Value of type variable "ProviderVar" of "mark_provider" cannot be "Callable[[str], int]" [type-var] ino.register_provider(func1) # E: Argument 1 to "register_provider" has incompatible type "Callable[[str], int]"; expected "Callable[[], Any]" [arg-type] ino.register(providers=[(func1,)]) # E: List item 0 has incompatible type "Tuple[Callable[[str], int]]"; expected "Union[Tuple[Callable[[], Any]], Tuple[Callable[[], Any], object], Tuple[Callable[[], Any], object, float]]" [list-item] # works also with store methods store.mark_provider(func1) # E: Value of type variable "ProviderVar" of "mark_provider" of "Store" cannot be "Callable[[str], int]" [type-var] store.register_provider(func1) # E: Argument 1 to "register_provider" of "Store" has incompatible type "Callable[[str], int]"; expected "Callable[[], Any]" [arg-type] store.register(providers=[(func1,)]) # E: List item 0 has incompatible type "Tuple[Callable[[str], int]]"; expected "Union[Tuple[Callable[[], Any]], Tuple[Callable[[], Any], object], Tuple[Callable[[], Any], object, float]]" [list-item] def func2() -> int: ... outfunc2 = ino.mark_provider(func2) # func2 is fine # make sure decorators didn't ruin types of decorated funcs reveal_type(outfunc1) # R: def (x: builtins.str) -> builtins.int reveal_type(outfunc2) # R: def () -> builtins.int @pytest.mark.mypy_testing def mypy_test_processor() -> None: store = ino.Store('name') def func1() -> int: ... # func1 takes no arguments, and so cannot be a processor outfunc1 = ino.mark_processor(func1) # E: Value of type variable "ProcessorVar" of "mark_processor" cannot be "Callable[[], int]" [type-var] ino.register_processor(func1) # E: Argument 1 to "register_processor" has incompatible type "Callable[[], int]"; expected "Callable[[Any], Any]" [arg-type] ino.register(processors=[(func1,)]) # E: List item 0 has incompatible type "Tuple[Callable[[], int]]"; expected "Union[Tuple[Callable[[Any], Any]], Tuple[Callable[[Any], Any], object], Tuple[Callable[[Any], Any], object, float]]" [list-item] # works also with store methods store.mark_processor(func1) # E: Value of type variable "ProcessorVar" of "mark_processor" of "Store" cannot be "Callable[[], int]" [type-var] store.register_processor(func1) # E: Argument 1 to "register_processor" of "Store" has incompatible type "Callable[[], int]"; expected "Callable[[Any], Any]" [arg-type] store.register(processors=[(func1,)]) # E: List item 0 has incompatible type "Tuple[Callable[[], int]]"; expected "Union[Tuple[Callable[[Any], Any]], Tuple[Callable[[Any], Any], object], Tuple[Callable[[Any], Any], object, float]]" [list-item] def func2(x: int) -> int: ... outfunc2 = ino.mark_processor(func2) # func2 is fine # make sure decorators didn't ruin types of decorated funcs reveal_type(outfunc1) # R: def () -> builtins.int reveal_type(outfunc2) # R: def (x: builtins.int) -> builtins.int in_n_out-0.1.8/.gitignore0000644000000000000000000000230613615410400012243 0ustar00# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # IDE settings .vscode/ src/**/*.c .asv/results in_n_out-0.1.8/LICENSE0000644000000000000000000000275113615410400011264 0ustar00BSD License Copyright (c) 2022, Talley Lambert All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. in_n_out-0.1.8/README.md0000644000000000000000000000441213615410400011532 0ustar00# in-n-out [![License](https://img.shields.io/pypi/l/in-n-out.svg?color=green)](https://github.com/pyapp-kit/in-n-out/raw/main/LICENSE) [![PyPI](https://img.shields.io/pypi/v/in-n-out.svg?color=green)](https://pypi.org/project/in-n-out) [![Python Version](https://img.shields.io/pypi/pyversions/in-n-out.svg?color=green)](https://python.org) [![CI](https://github.com/pyapp-kit/in-n-out/actions/workflows/ci.yml/badge.svg)](https://github.com/pyapp-kit/in-n-out/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/pyapp-kit/in-n-out/branch/main/graph/badge.svg)](https://app.codecov.io/gh/pyapp-kit/in-n-out) [![Benchmarks](https://img.shields.io/badge/⏱-codspeed-%23FF7B53)](https://codspeed.io/pyapp-kit/in-n-out) Python dependency injection you can taste. A lightweight dependency injection and result processing framework for Python using type hints. Emphasis is on simplicity, ease of use, and minimal impact on source code. ```python import in_n_out as ino class Thing: def __init__(self, name: str): self.name = name # use ino.inject to create a version of the function # that will retrieve the required dependencies at call time @ino.inject def func(thing: Thing): return thing.name def give_me_a_thing() -> Thing: return Thing("Thing") # register a provider of Thing ino.register_provider(give_me_a_thing) print(func()) # prints "Thing" def give_me_another_thing() -> Thing: return Thing("Another Thing") with ino.register_provider(give_me_another_thing, weight=10): print(func()) # prints "Another Thing" ``` This also supports processing *return* values as well (injection of intentional side effects): ```python @ino.inject_processors def func2(thing: Thing) -> str: return thing.name def greet_name(name: str): print(f"Hello, {name}!") ino.register_processor(greet_name) func2(Thing('Bob')) # prints "Hello, Bob!" ``` ### Alternatives Lots of other python DI frameworks exist, here are a few alternatives to consider: - - - - - - - in_n_out-0.1.8/pyproject.toml0000644000000000000000000000703313615410400013171 0ustar00# https://peps.python.org/pep-0517/ [build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" # https://peps.python.org/pep-0621/ [project] name = "in-n-out" description = " plugable dependency injection and result processing" readme = "README.md" requires-python = ">=3.8" license = { text = "BSD 3-Clause License" } authors = [{ email = "talley.lambert@gmail.com", name = "Talley Lambert" }] classifiers = [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Typing :: Typed", ] dynamic = ["version"] dependencies = [] # https://hatch.pypa.io/latest/config/metadata/ [tool.hatch.version] source = "vcs" [tool.hatch.build.targets.sdist] include = ["/src", "/tests"] [tool.hatch.build.targets.wheel] only-include = ["src"] sources = ["src"] # extras # https://peps.python.org/pep-0621/#dependencies-optional-dependencies [project.optional-dependencies] test = ["pytest>=6.0", "pytest-cov", "toolz", "pytest-codspeed"] dev = [ "black", "ruff", "ipython", "mypy", "pdbpp", "pre-commit", "pydocstyle", "pytest-cov", "pytest", "rich", ] [project.urls] homepage = "https://github.com/pyapp-kit/in-n-out" repository = "https://github.com/pyapp-kit/in-n-out" documentation = "https://pyapp-kit.github.io/in-n-out" # https://github.com/charliermarsh/ruff [tool.ruff] line-length = 88 src = ["src", "tests"] target-version = "py38" select = [ "E", # style errors "F", # flakes "D", # pydocstyle "I", # isort "UP", # pyupgrade "S", # bandit "C4", # flake8-comprehensions "B", # flake8-bugbear "A001", # flake8-builtins "RUF", # ruff-specific rules "TCH", # flake8-typing-imports ] ignore = [ "D100", # Missing docstring in public module "D107", # Missing docstring in __init__ "D203", # 1 blank line required before class docstring "D212", # Multi-line docstring summary should start at the first line "D213", # Multi-line docstring summary should start at the second line "D401", # First line should be in imperative mood "D413", # Missing blank line after last section "D416", # Section name should end with a colon "C901", # complexity ] [tool.ruff.per-file-ignores] "tests/*.py" = ["D", "S"] "benchmarks/*.py" = ["D"] "setup.py" = ["D"] "src/in_n_out/_global.py" = ["D"] # https://docs.pytest.org/en/6.2.x/customize.html [tool.pytest.ini_options] minversion = "6.0" testpaths = ["tests"] filterwarnings = ["error"] [tool.coverage.run] source = ['src/in_n_out'] command_line = "-m pytest" # https://coverage.readthedocs.io/en/6.4/config.html [tool.coverage.report] exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", "@overload", "except ImportError", ] show_missing = true # https://mypy.readthedocs.io/en/stable/config_file.html [tool.mypy] files = "src/**/*.py" strict = true disallow_any_generics = false show_error_codes = true pretty = true [[tool.mypy.overrides]] module = ["tests"] disallow_untyped_defs = false # https://github.com/mgedmin/check-manifest#configuration [tool.check-manifest] ignore = [ ".cruft.json", ".flake8", ".github_changelog_generator", ".pre-commit-config.yaml", "tests/**/*", "**/*.c", "Makefile", "codecov.yml", "asv.conf.json", "benchmarks/**/*", "CHANGELOG.md", ] in_n_out-0.1.8/PKG-INFO0000644000000000000000000000726113615410400011355 0ustar00Metadata-Version: 2.1 Name: in-n-out Version: 0.1.8 Summary: plugable dependency injection and result processing Project-URL: homepage, https://github.com/pyapp-kit/in-n-out Project-URL: repository, https://github.com/pyapp-kit/in-n-out Project-URL: documentation, https://pyapp-kit.github.io/in-n-out Author-email: Talley Lambert License: BSD 3-Clause License License-File: LICENSE Classifier: Development Status :: 3 - Alpha Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Typing :: Typed Requires-Python: >=3.8 Provides-Extra: dev Requires-Dist: black; extra == 'dev' Requires-Dist: ipython; extra == 'dev' Requires-Dist: mypy; extra == 'dev' Requires-Dist: pdbpp; extra == 'dev' Requires-Dist: pre-commit; extra == 'dev' Requires-Dist: pydocstyle; extra == 'dev' Requires-Dist: pytest; extra == 'dev' Requires-Dist: pytest-cov; extra == 'dev' Requires-Dist: rich; extra == 'dev' Requires-Dist: ruff; extra == 'dev' Provides-Extra: test Requires-Dist: pytest-codspeed; extra == 'test' Requires-Dist: pytest-cov; extra == 'test' Requires-Dist: pytest>=6.0; extra == 'test' Requires-Dist: toolz; extra == 'test' Description-Content-Type: text/markdown # in-n-out [![License](https://img.shields.io/pypi/l/in-n-out.svg?color=green)](https://github.com/pyapp-kit/in-n-out/raw/main/LICENSE) [![PyPI](https://img.shields.io/pypi/v/in-n-out.svg?color=green)](https://pypi.org/project/in-n-out) [![Python Version](https://img.shields.io/pypi/pyversions/in-n-out.svg?color=green)](https://python.org) [![CI](https://github.com/pyapp-kit/in-n-out/actions/workflows/ci.yml/badge.svg)](https://github.com/pyapp-kit/in-n-out/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/pyapp-kit/in-n-out/branch/main/graph/badge.svg)](https://app.codecov.io/gh/pyapp-kit/in-n-out) [![Benchmarks](https://img.shields.io/badge/⏱-codspeed-%23FF7B53)](https://codspeed.io/pyapp-kit/in-n-out) Python dependency injection you can taste. A lightweight dependency injection and result processing framework for Python using type hints. Emphasis is on simplicity, ease of use, and minimal impact on source code. ```python import in_n_out as ino class Thing: def __init__(self, name: str): self.name = name # use ino.inject to create a version of the function # that will retrieve the required dependencies at call time @ino.inject def func(thing: Thing): return thing.name def give_me_a_thing() -> Thing: return Thing("Thing") # register a provider of Thing ino.register_provider(give_me_a_thing) print(func()) # prints "Thing" def give_me_another_thing() -> Thing: return Thing("Another Thing") with ino.register_provider(give_me_another_thing, weight=10): print(func()) # prints "Another Thing" ``` This also supports processing *return* values as well (injection of intentional side effects): ```python @ino.inject_processors def func2(thing: Thing) -> str: return thing.name def greet_name(name: str): print(f"Hello, {name}!") ino.register_processor(greet_name) func2(Thing('Bob')) # prints "Hello, Bob!" ``` ### Alternatives Lots of other python DI frameworks exist, here are a few alternatives to consider: - - - - - - -