From b0152671be2563bfe46060df11bcf1ad1019a490 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 29 Jun 2022 19:23:15 -0400 Subject: [PATCH] feat: make multiple store instances accessible (#6) * feat: adding store to all funcs * refactor: rearrange * docs: fix docstrings * test: more tests * test: add clear tests --- src/in_n_out/__init__.py | 2 + src/in_n_out/_processors.py | 172 +++++++++++++++++++++++++----------- src/in_n_out/_providers.py | 172 ++++++++++++++++++++++++------------ src/in_n_out/_store.py | 87 +++++++++++++++++- tests/test_processors.py | 6 +- tests/test_providers.py | 8 +- tests/test_store.py | 57 ++++++++++++ 7 files changed, 386 insertions(+), 118 deletions(-) create mode 100644 tests/test_store.py diff --git a/src/in_n_out/__init__.py b/src/in_n_out/__init__.py index 5947a3c..fbc9662 100644 --- a/src/in_n_out/__init__.py +++ b/src/in_n_out/__init__.py @@ -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, @@ -30,5 +31,6 @@ "resolve_type_hints", "set_processors", "set_providers", + "Store", "type_resolved_signature", ] diff --git a/src/in_n_out/_processors.py b/src/in_n_out/_processors.py index cbad8a8..6ede877 100644 --- a/src/in_n_out/_processors.py +++ b/src/in_n_out/_processors.py @@ -2,6 +2,7 @@ from typing import ( Any, Callable, + Literal, Mapping, Optional, Type, @@ -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 @@ -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( @@ -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 diff --git a/src/in_n_out/_providers.py b/src/in_n_out/_providers.py index e35b541..9f37607 100644 --- a/src/in_n_out/_providers.py +++ b/src/in_n_out/_providers.py @@ -3,6 +3,7 @@ Any, Callable, Dict, + Literal, Optional, Type, Union, @@ -11,27 +12,54 @@ overload, ) -from ._store import _STORE, Provider, T +from ._store import Provider, Store, T -def provider(func: Provider) -> Provider: - """Decorator that declares `func` as a provider of its return type. +class set_providers: + """Set provider(s) for given type(s). - Note, If func returns `Optional[Type]`, it will be registered as a provider - for Type. + "Providers" are functions that can retrieve an instance of a given type. - Examples - -------- - >>> @provider - >>> def provides_int() -> int: - ... return 42 + This is a class that behaves as a function or a context manager, that + allows one to set a provider function for a given type. + + Parameters + ---------- + mapping : Dict[Type[T], Callable[..., Optional[T]]] + a map of type -> provider 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 provider function, by default False. + store : Union[str, Store, None] + The provider store to use, if not provided the global store is used. + + Raises + ------ + ValueError + if clobber is `False` and one of the keys in `mapping` is already + registered. """ - return_hint = get_type_hints(func).get("return") - if return_hint is None: - warnings.warn(f"{func} has no return type hint. Cannot be a processor.") - else: - set_providers({return_hint: func}) - return func + + def __init__( + self, + mapping: Dict[Type[T], Union[T, Callable[[], T]]], + *, + clobber: bool = False, + store: Union[str, Store, None] = None, + ) -> None: + self._store = store if isinstance(store, Store) else Store.get_store(store) + self._before = self._store._set(mapping, provider=True, clobber=clobber) + + def __enter__(self) -> None: + return None + + def __exit__(self, *_: Any) -> None: + for (type_, optional), val in self._before.items(): + MAP: dict = self._store.opt_providers if optional else self._store.providers + if val is self._store._NULL: + del MAP[type_] + else: + MAP[type_] = cast(Callable, val) @overload @@ -46,24 +74,39 @@ def get_provider(type_: object) -> Union[Callable[[], Optional[T]], None]: def get_provider( - type_: Union[object, Type[T]] + type_: Union[object, Type[T]], store: Union[str, Store, None] = None ) -> Union[Callable[[], T], Callable[[], Optional[T]], None]: """Return object provider function given a type. An object provider is a function that returns an instance of a particular object type. - This is a form of dependency injection, and, along with - `inject_dependencies`, allows us to inject objects into functions based on - type hints. + Parameters + ---------- + type_ : Type[T] or Type Hint + Type for which to get the provider. + store : Union[str, Store, None] + The provider store to use, if not provided the global store is used. + + Returns + ------- + Optional[Callable[[T], Any]] + A provider function registered for `type_`, if any. + + Examples + -------- + >>> get_provider(int) """ - return _get_provider(type_, pop=False) + return _get_provider(type_, pop=False, store=store) def _get_provider( - type_: Union[object, Type[T]], pop: bool = False + type_: Union[object, Type[T]], + pop: bool = False, + store: Union[str, Store, None] = None, ) -> Union[Callable[[], T], Callable[[], Optional[T]], None]: - return _STORE._get(type_, provider=True, pop=pop) + store = store if isinstance(store, Store) else Store.get_store(store) + return store._get(type_, provider=True, pop=pop) @overload @@ -77,7 +120,9 @@ def clear_provider(type_: object) -> Union[Callable[[], Optional[T]], None]: def clear_provider( - 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. @@ -91,13 +136,15 @@ def clear_provider( The provider 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 provider store to use, if not provided the global store is used. Returns ------- Optional[Callable[[], T]] The provider function that was cleared, if any. """ - result = _get_provider(type_, pop=True) + result = _get_provider(type_, pop=True, store=store) if result is None and warn_missing: warnings.warn( @@ -106,44 +153,55 @@ def clear_provider( return result -class set_providers: - """Set provider(s) for given type(s). +# Decorator - "Providers" are functions that can retrieve an instance of a given type. - This is a class that behaves as a function or a context manager, that - allows one to set a provider function for a given type. +@overload +def provider(func: Provider, *, store: Union[str, Store, None] = None) -> Provider: + ... + + +@overload +def provider( + func: Literal[None] = ..., *, store: Union[str, Store, None] = None +) -> Callable[[Provider], Provider]: + ... + + +def provider( + func: Optional[Provider] = None, *, store: Union[str, Store, None] = None +) -> Union[Callable[[Provider], Provider], Provider]: + """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 ---------- - mapping : Dict[Type[T], Callable[..., Optional[T]]] - a map of type -> provider 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 provider function, by default False. + func : Optional[Provider], optional + A function to decorate. If not provided, a decorator is returned. + store : Union[str, Store, None] + The Provider store to use, if not provided the global store is used. - Raises - ------ - ValueError - if clobber is `False` and one of the keys in `mapping` is already - registered. - """ - - def __init__( - self, - mapping: Dict[Type[T], Union[T, Callable[[], T]]], - clobber: bool = False, - ) -> None: - self._before = _STORE._set(mapping, provider=True, clobber=clobber) + Returns + ------- + Union[Callable[[Provider], Provider], Provider] + If `func` is not provided, a decorator is returned, if `func` is provided + then the function is returned.. - def __enter__(self) -> None: - return None + Examples + -------- + >>> @provider + >>> def provide_int() -> int: + ... return 42 + """ - def __exit__(self, *_: Any) -> None: + def _inner(func: Provider) -> Provider: + return_hint = get_type_hints(func).get("return") + if return_hint is None: + warnings.warn(f"{func} has no return type hint. Cannot be a provider.") + else: + set_providers({return_hint: func}, store=store) + return func - for (type_, optional), val in self._before.items(): - MAP: dict = _STORE.opt_providers if optional else _STORE.providers - if val is _STORE._NULL: - del MAP[type_] - else: - MAP[type_] = cast(Callable, val) + return _inner(func) if func is not None else _inner diff --git a/src/in_n_out/_store.py b/src/in_n_out/_store.py index 14cc58a..f5c8354 100644 --- a/src/in_n_out/_store.py +++ b/src/in_n_out/_store.py @@ -17,16 +17,95 @@ T = TypeVar("T") Provider = TypeVar("Provider", bound=Callable[[], Any]) Processor = TypeVar("Processor", bound=Callable[[Any], Any]) +_GLOBAL = "global" -class _Store: - _NULL = object() +class Store: + """A Store is a collection of providers and processors.""" - def __init__(self) -> None: + _NULL = object() + _instances: 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: Optional[str] = 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`.""" + 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: Dict[Type, Callable[[], Any]] = {} self.opt_providers: Dict[Type, Callable[[], Optional[Any]]] = {} self.processors: Dict[Any, Callable[[Any], Any]] = {} + @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.opt_providers.clear() + self.processors.clear() + def _get( self, type_: Union[object, Type[T]], provider: bool, pop: bool ) -> Optional[Callable]: @@ -106,4 +185,4 @@ def _set( return _before -_STORE = _Store() +Store._instances[_GLOBAL] = Store(_GLOBAL) diff --git a/tests/test_processors.py b/tests/test_processors.py index cbb1d47..6bda7f1 100644 --- a/tests/test_processors.py +++ b/tests/test_processors.py @@ -2,9 +2,9 @@ import pytest -from in_n_out import get_processor, processor, set_processors +from in_n_out import Store, get_processor, processor, set_processors from in_n_out._processors import clear_processor -from in_n_out._store import _STORE +from in_n_out._store import _GLOBAL @pytest.mark.parametrize( @@ -90,7 +90,7 @@ def processes_string(x: str): assert clear_processor(str) is processes_string # all clear - assert not _STORE.processors + assert not Store.get_store(_GLOBAL).processors def test_unlikely_processor(): diff --git a/tests/test_providers.py b/tests/test_providers.py index e245155..50dddd3 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -2,9 +2,9 @@ import pytest -from in_n_out import get_provider, provider, set_providers +from in_n_out import Store, get_provider, provider, set_providers from in_n_out._providers import clear_provider -from in_n_out._store import _STORE +from in_n_out._store import _GLOBAL @pytest.mark.parametrize( @@ -97,8 +97,8 @@ def provides_int() -> int: assert clear_provider(str) is provides_str # all clear - assert not _STORE.opt_providers - assert not _STORE.providers + assert not Store.get_store(_GLOBAL).opt_providers + assert not Store.get_store(_GLOBAL).providers def test_unlikely_provider(): diff --git a/tests/test_store.py b/tests/test_store.py new file mode 100644 index 0000000..9497708 --- /dev/null +++ b/tests/test_store.py @@ -0,0 +1,57 @@ +from typing import Optional + +import pytest + +from in_n_out import Store, set_processors, set_providers +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.create("test") + assert not test_store.providers + assert not test_store.opt_providers + assert not test_store.processors + + set_providers({int: 1}, store=test_store) + set_providers({Optional[str]: None}, store=test_store) + set_processors({int: print}, store=test_store) + assert len(test_store.providers) == 1 + assert len(test_store.opt_providers) == 1 + assert len(test_store.processors) == 1 + + test_store.clear() + assert not test_store.providers + assert not test_store.opt_providers + assert not test_store.processors