Skip to content

Commit

Permalink
feat: make multiple store instances accessible (#6)
Browse files Browse the repository at this point in the history
* feat: adding store to all funcs

* refactor: rearrange

* docs: fix docstrings

* test: more tests

* test: add clear tests
  • Loading branch information
tlambert03 authored Jun 29, 2022
1 parent c2e28c3 commit b015267
Show file tree
Hide file tree
Showing 7 changed files with 386 additions and 118 deletions.
2 changes: 2 additions & 0 deletions src/in_n_out/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from ._inject import inject_dependencies
from ._processors import get_processor, processor, set_processors
from ._providers import get_provider, provider, set_providers
from ._store import Store
from ._type_resolution import (
resolve_single_type_hints,
resolve_type_hints,
Expand All @@ -30,5 +31,6 @@
"resolve_type_hints",
"set_processors",
"set_providers",
"Store",
"type_resolved_signature",
]
172 changes: 122 additions & 50 deletions src/in_n_out/_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import (
Any,
Callable,
Literal,
Mapping,
Optional,
Type,
Expand All @@ -11,30 +12,84 @@
overload,
)

from ._store import _STORE, Processor, T
from ._store import Processor, Store, T


def processor(func: Processor) -> Processor:
"""Decorator that declares `func` as a processor of its first parameter type."""
hints = get_type_hints(func)
hints.pop("return", None)
if not hints:
warnings.warn(f"{func} has no argument type hints. Cannot be a processor.")
return func
class set_processors:
"""Set processor(s) for given type(s).
"Processors" are functions that can "do something" with an instance of the
type that they support.
This is a class that behaves as a function or a context manager, that
allows one to set a processor function for a given type.
Parameters
----------
mapping : Dict[Type[T], Callable[..., Optional[T]]]
a map of type -> processor function, where each value is a function
that is capable of retrieving an instance of the associated key/type.
clobber : bool, optional
Whether to override any existing processor function, by default False.
store : Union[str, Store, None]
The processor store to use, if not provided the global store is used.
Raises
------
ValueError
if clobber is `True` and one of the keys in `mapping` is already
registered.
"""

hint0 = list(hints.values())[0]
set_processors({hint0: func})
return func
def __init__(
self,
mapping: Mapping[Any, Callable[[T], Any]],
*,
clobber: bool = False,
store: Union[str, Store, None] = None,
):
self._store = store if isinstance(store, Store) else Store.get_store(store)
self._before = self._store._set(mapping, provider=False, clobber=clobber)

def __enter__(self) -> None:
return None

def get_processor(type_: Type[T]) -> Optional[Callable[[T], Any]]:
def __exit__(self, *_: Any) -> None:
for (type_, _), val in self._before.items():
if val is self._store._NULL:
del self._store.processors[type_]
else:
self._store.processors[type_] = cast(Callable, val)


def get_processor(
type_: Type[T],
store: Union[str, Store, None] = None,
) -> Optional[Callable[[T], Any]]:
"""Return processor function for a given type.
A processor is a function that can "process" a given return type. The term
process here leaves a lot of ambiguity, it mostly means the function "can
do something" with a single input of the given type.
Parameters
----------
type_ : Type[T]
Type for which to get the processor.
store : Union[str, Store, None]
The processor store to use, if not provided the global store is used.
Returns
-------
Optional[Callable[[T], Any]]
A processor function registered for `type_`, if any.
Examples
--------
>>> get_processor(int)
"""
return _STORE._get(type_, provider=False, pop=False)
store = store if isinstance(store, Store) else Store.get_store(store)
return store._get(type_, provider=False, pop=False)


@overload
Expand All @@ -48,27 +103,32 @@ def clear_processor(type_: object) -> Union[Callable[[], Optional[T]], None]:


def clear_processor(
type_: Union[object, Type[T]], warn_missing: bool = False
type_: Union[object, Type[T]],
warn_missing: bool = False,
store: Union[str, Store, None] = None,
) -> Union[Callable[[], T], Callable[[], Optional[T]], None]:
"""Clear provider for a given type.
"""Clear processor for a given type.
Note: this does NOT yet clear sub/superclasses of type_. So if there is a registered
provider for Sequence, and you call clear_processor(list), the Sequence provider
will still be registered, and vice versa.
processor for `Sequence`, and you call `clear_processor(list)`, the `Sequence`
processor will still be registered, and vice versa.
Parameters
----------
type_ : Type[T]
The provider type to clear
The processor type to clear
warn_missing : bool, optional
Whether to emit a warning if there was not type registered, by default False
store : Union[str, Store, None]
The processor store to use, if not provided the global store is used.
Returns
-------
Optional[Callable[[], T]]
The provider function that was cleared, if any.
The processor function that was cleared, if any.
"""
result = _STORE._get(type_, provider=False, pop=True)
store = store if isinstance(store, Store) else Store.get_store(store)
result = store._get(type_, provider=False, pop=True)

if result is None and warn_missing:
warnings.warn(
Expand All @@ -77,43 +137,55 @@ def clear_processor(
return result


class set_processors:
"""Set processor(s) for given type(s).
# Decorator

"Processors" are functions that can "do something" with an instance of the
type that they support.

This is a class that behaves as a function or a context manager, that
allows one to set a processor function for a given type.
@overload
def processor(func: Processor, *, store: Union[str, Store, None] = None) -> Processor:
...


@overload
def processor(
func: Literal[None] = ..., *, store: Union[str, Store, None] = None
) -> Callable[[Processor], Processor]:
...


def processor(
func: Optional[Processor] = None, *, store: Union[str, Store, None] = None
) -> Union[Callable[[Processor], Processor], Processor]:
"""Decorate `func` as a processor of its first parameter type.
Parameters
----------
mapping : Dict[Type[T], Callable[..., Optional[T]]]
a map of type -> processor function, where each value is a function
that is capable of retrieving an instance of the associated key/type.
clobber : bool, optional
Whether to override any existing processor function, by default False.
func : Optional[Processor], optional
A function to decorate. If not provided, a decorator is returned.
store : Union[str, Store, None]
The processor store to use, if not provided the global store is used.
Raises
------
ValueError
if clobber is `True` and one of the keys in `mapping` is already
registered.
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
--------
>>> @processor
>>> def process_int(x: int) -> None:
... print("Processing int:", x)
"""

def __init__(
self, mapping: Mapping[Any, Callable[[T], Any]], clobber: bool = False
):
self._before = _STORE._set(mapping, provider=False, clobber=clobber)

def __enter__(self) -> None:
return None
def _inner(func: Processor) -> Processor:
hints = get_type_hints(func)
hints.pop("return", None)
if not hints:
warnings.warn(f"{func} has no argument type hints. Cannot be a processor.")
return func

def __exit__(self, *_: Any) -> None:
hint0 = list(hints.values())[0]
set_processors({hint0: func}, store=store)
return func

for (type_, _), val in self._before.items():
MAP: dict = _STORE.processors
if val is _STORE._NULL:
del MAP[type_]
else:
MAP[type_] = cast(Callable, val)
return _inner(func) if func is not None else _inner
Loading

0 comments on commit b015267

Please sign in to comment.