Skip to content

Commit

Permalink
feat: split decorator into 2 (#20)
Browse files Browse the repository at this point in the history
* feat: split decorator

* test: split working
  • Loading branch information
tlambert03 committed Jul 6, 2022
1 parent fb5e562 commit 176a792
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 17 deletions.
3 changes: 2 additions & 1 deletion src/in_n_out/__init__.py
Expand Up @@ -9,7 +9,7 @@
__author__ = "Talley Lambert"
__email__ = "talley.lambert@gmail.com"

from ._inject import inject_dependencies
from ._inject import inject_dependencies, process_output
from ._processors import iter_processors, processor, set_processors
from ._providers import iter_providers, provider, set_providers
from ._store import Store
Expand All @@ -26,6 +26,7 @@
"iter_providers",
"inject_dependencies",
"processor",
"process_output",
"provider",
"resolve_single_type_hints",
"resolve_type_hints",
Expand Down
66 changes: 62 additions & 4 deletions src/in_n_out/_inject.py
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Union, overload
from typing import TYPE_CHECKING, Type, Union, overload

from ._store import Store

Expand All @@ -13,6 +13,7 @@

P = ParamSpec("P")
R = TypeVar("R")
T = TypeVar("T")
OptRaiseWarnReturnIgnore = Optional[RaiseWarnReturnIgnore]


Expand All @@ -24,7 +25,8 @@ def inject_dependencies(
store: Union[str, Store, None] = None,
on_unresolved_required_args: OptRaiseWarnReturnIgnore = None,
on_unannotated_required_args: OptRaiseWarnReturnIgnore = None,
) -> Callable[P, R]:
process_output: bool = False,
) -> Callable[..., R]:
...


Expand All @@ -36,7 +38,8 @@ def inject_dependencies(
store: Union[str, Store, None] = None,
on_unresolved_required_args: OptRaiseWarnReturnIgnore = None,
on_unannotated_required_args: OptRaiseWarnReturnIgnore = None,
) -> Callable[[Callable[P, R]], Callable[P, R]]:
process_output: bool = False,
) -> Callable[[Callable[..., R]], Callable[..., R]]:
...


Expand All @@ -47,7 +50,8 @@ def inject_dependencies(
store: Union[str, Store, None] = None,
on_unresolved_required_args: OptRaiseWarnReturnIgnore = None,
on_unannotated_required_args: OptRaiseWarnReturnIgnore = None,
) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]:
process_output: bool = False,
) -> Union[Callable[..., R], Callable[[Callable[..., R]], Callable[..., R]]]:
"""Decorator returns func that can access/process objects based on type hints.
This is form of dependency injection, and result processing. It does 2 things:
Expand Down Expand Up @@ -92,6 +96,13 @@ def inject_dependencies(
- 'return': stop decorating, return the original function without warning
- 'ignore': continue decorating without warning.
process_output : bool
Whether to additionally "inject" output processing into the function
being decorated. If used, the output will be additionally decorated with
self.process_output before returning. `process_output` can also be used
on its, if it is desired to *only* process outputs, but not inject
dependencies.
Returns
-------
Callable
Expand All @@ -103,4 +114,51 @@ def inject_dependencies(
localns=localns,
on_unresolved_required_args=on_unresolved_required_args,
on_unannotated_required_args=on_unannotated_required_args,
process_output=process_output,
)


def process_output(
func: Optional[Callable[P, R]] = None,
*,
hint: Union[object, Type[T], None] = None,
first_processor_only: bool = False,
raise_exception: bool = False,
store: Union[str, Store, None] = None,
) -> Union[Callable[[Callable[P, R]], Callable[P, R]], Callable[P, R]]:
"""Decorate a function to process its output.
When the decorated function is called, the return value will be processed
with `store.process(return_value)` before returning the result.
Parameters
----------
func : Optional[Callable]
A function to decorate
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.
store : Union[str, Store, None]
Optional store to use when retrieving providers and processors,
by default the global store will be used.
Returns
-------
Callable
A function that, when called, will have its return value processed by
`store.process(return_value)`
"""
_store = store if isinstance(store, Store) else Store.get_store(store)
return _store.process_output(
func=func,
hint=hint,
first_processor_only=first_processor_only,
raise_exception=raise_exception,
)
115 changes: 109 additions & 6 deletions src/in_n_out/_store.py
Expand Up @@ -135,7 +135,20 @@ def get_store(cls, name: Optional[str] = None) -> Store:

@classmethod
def destroy(cls, name: str) -> None:
"""Destroy Store instance with the given `name`."""
"""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")
Expand Down Expand Up @@ -181,6 +194,8 @@ def namespace(self) -> Dict[str, object]:
def namespace(self, ns: Union[Namespace, Callable[[], Namespace]]) -> None:
self._namespace = ns

# -------------------------------------------------------------------------

def register_provider(
self,
provider: Provider,
Expand Down Expand Up @@ -511,6 +526,7 @@ def inject_dependencies(
localns: Optional[dict] = None,
on_unresolved_required_args: Optional[RaiseWarnReturnIgnore] = None,
on_unannotated_required_args: Optional[RaiseWarnReturnIgnore] = None,
process_output: bool = False,
) -> Callable[P, R]:
...

Expand All @@ -522,6 +538,7 @@ def inject_dependencies(
localns: Optional[dict] = None,
on_unresolved_required_args: Optional[RaiseWarnReturnIgnore] = None,
on_unannotated_required_args: Optional[RaiseWarnReturnIgnore] = None,
process_output: bool = False,
) -> Callable[[Callable[P, R]], Callable[P, R]]:
...

Expand All @@ -532,6 +549,7 @@ def inject_dependencies(
localns: Optional[dict] = None,
on_unresolved_required_args: Optional[RaiseWarnReturnIgnore] = None,
on_unannotated_required_args: Optional[RaiseWarnReturnIgnore] = None,
process_output: bool = False,
) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]:
"""Decorator returns func that can access/process objects based on type hints.
Expand Down Expand Up @@ -574,6 +592,13 @@ def inject_dependencies(
- 'return': immediately return the original function without warning
- 'ignore': continue decorating without warning.
process_output : bool
Whether to additionally "inject" output processing into the function
being decorated. If used, the output will be additionally decorated with
self.process_output before returning. `process_output` can also be used
on its, if it is desired to *only* process outputs, but not inject
dependencies.
Returns
-------
Callable
Expand Down Expand Up @@ -605,7 +630,6 @@ def _inner(func: Callable[P, R]) -> Callable[P, R]:
)
if sig is None: # something went wrong, and the user was notified.
return func
process_result = sig.return_annotation is not sig.empty

# get provider functions for each required parameter
@wraps(func)
Expand Down Expand Up @@ -636,10 +660,6 @@ def _exec(*args: P.args, **kwargs: P.kwargs) -> R:
f"{e}"
) from e

if result is not None and process_result:
# TODO: pass on keywords
self.process(result, hint=_sig.return_annotation)

return result

out = _exec
Expand All @@ -662,10 +682,93 @@ def _gexec(*args: P.args, **kwargs: P.kwargs) -> R: # type: ignore
out.__doc__ = (
out.__doc__ or ""
) + "\n\n*This function will inject dependencies when called.*"

if process_output and sig.return_annotation is not sig.empty:
return self.process_output(out, hint=sig.return_annotation)
return out

return _inner(func) if func is not None else _inner

@overload
def process_output(
self,
func: Callable[P, R],
*,
hint: Union[object, Type[T], None] = None,
first_processor_only: bool = False,
raise_exception: bool = False,
) -> Callable[P, R]:
...

@overload
def process_output(
self,
func: Literal[None] = None,
*,
hint: Union[object, Type[T], None] = None,
first_processor_only: bool = False,
raise_exception: bool = False,
) -> Callable[[Callable[P, R]], Callable[P, R]]:
...

def process_output(
self,
func: Optional[Callable[P, R]] = None,
*,
hint: Union[object, Type[T], None] = None,
first_processor_only: bool = False,
raise_exception: bool = False,
) -> Union[Callable[[Callable[P, R]], Callable[P, R]], Callable[P, R]]:
"""Decorate a function to process its output.
When the decorated function is called, the return value will be processed
with `store.process(return_value)` before returning the result.
Parameters
----------
func : Optional[Callable]
A function to decorate
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]:
nonlocal hint
if hint is None:
annotations = getattr(func, "__annotations__", {})
if "return" in annotations:
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,
hint=hint,
first_processor_only=first_processor_only,
raise_exception=raise_exception,
)
return result

return _exec

return _deco(func) if func is not None else _deco

# -----------------

@cached_property
Expand Down
40 changes: 34 additions & 6 deletions tests/test_injection.py
Expand Up @@ -4,7 +4,13 @@

import pytest

from in_n_out import inject_dependencies, set_processors, set_providers
from in_n_out import (
Store,
inject_dependencies,
process_output,
set_processors,
set_providers,
)


def test_injection():
Expand All @@ -16,6 +22,29 @@ def f(i: int, s: str):
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_dependencies(f, process_output=True)
elif order == "inject_first":
f = process_output(inject_dependencies(f))
elif order == "inject_last":
f = inject_dependencies(process_output(f))

with set_providers({int: lambda: 1}):
with set_processors({str: mock2}):
assert f() == "1"
mock.assert_called_once_with(1)
mock2.assert_called_once_with("1")


def test_injection_missing():
@inject_dependencies
def f(x: int):
Expand All @@ -30,7 +59,7 @@ def f(x: int):


def test_set_processor():
@inject_dependencies
@process_output
def f2(x: int) -> int:
return x

Expand All @@ -39,7 +68,6 @@ def f2(x: int) -> int:
mock = Mock()

def process_int(x: int) -> None:
print("HI")
mock(x)

with set_processors({int: process_int}):
Expand Down Expand Up @@ -131,8 +159,8 @@ def test_injection_errors(in_func, on_unresolved, on_unannotated):
assert out_func is not in_func


def test_processors_not_passed_none():
@inject_dependencies
def test_processors_not_passed_none(test_store: Store):
@test_store.process_output
def f(x: int) -> Optional[int]:
return x if x > 5 else None

Expand All @@ -144,7 +172,7 @@ def f(x: int) -> Optional[int]:
def process_int(x: int) -> None:
mock(x)

with set_processors({int: process_int}):
with set_processors({int: process_int}, store=test_store):
assert f(3) is None
mock.assert_not_called()
assert f(10) == 10
Expand Down

0 comments on commit 176a792

Please sign in to comment.