Skip to content

Commit

Permalink
more typign
Browse files Browse the repository at this point in the history
  • Loading branch information
tlambert03 committed Mar 18, 2024
1 parent 78f96d9 commit 90f5b53
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 86 deletions.
4 changes: 2 additions & 2 deletions src/in_n_out/_global.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def mark_processor(

@_add_store_to_doc
def iter_providers(
type_hint: object | type[T], store: str | Store | None = None
type_hint: type[T], store: str | Store | None = None
) -> Iterable[Callable[[], T | None]]:
"""Iterate over all providers of `type_hint` in `store` or the global store.
Expand All @@ -190,7 +190,7 @@ def iter_processors(

@_add_store_to_doc
def provide(
type_hint: object | type[T],
type_hint: type[T],
store: str | Store | None = None,
) -> T | None:
"""Provide an instance of `type_hint` with providers from `store` or the global store.
Expand Down
51 changes: 28 additions & 23 deletions src/in_n_out/_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
ClassVar,
ContextManager,
Iterable,
Iterator,
Literal,
Mapping,
NamedTuple,
Expand Down Expand Up @@ -52,23 +53,27 @@

Disposer = Callable[[], None]
Namespace = Mapping[str, object]
HintArg = object
THint = Any
Weight = float

# (callback,)
# (callback, type_hint)
# (callback, type_hint, weight)
ProviderTuple = Union[
Tuple[Provider], Tuple[Provider, HintArg], Tuple[Provider, HintArg, Weight]
Tuple[Provider], Tuple[Provider, THint], Tuple[Provider, THint, Weight]
]
ProcessorTuple = Union[
Tuple[Processor], Tuple[Processor, HintArg], Tuple[Processor, HintArg, Weight]
Tuple[Processor], Tuple[Processor, THint], Tuple[Processor, THint, 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]]
ProviderIterable = Union[
Iterable[ProviderTuple], # e.g. register(providers=[(lambda: 1, int)])
Mapping[THint, Provider], # e.g. register(providers={int: lambda: 1})
Mapping[THint, object], # e.g. register(providers={int: 1})
]
ProcessorIterable = Union[Iterable[ProcessorTuple], Mapping[THint, Processor]]
CallbackIterable = Union[ProviderIterable, ProcessorIterable]

_GLOBAL = "global"
Expand Down Expand Up @@ -475,13 +480,13 @@ def _deco(func: ProcessorVar) -> ProcessorVar:
# ------------------------- Callback retrieval ------------------------------

def iter_providers(
self, type_hint: object | type[T]
) -> Iterable[Callable[[], T | None]]:
self, type_hint: type[T] | object
) -> Iterator[Callable[[], T | None]]:
"""Iterate over all providers of `type_hint`.
Parameters
----------
type_hint : object | Type[T]
type_hint : type[T] | object
A type or type hint for which to return providers.
Yields
Expand All @@ -492,13 +497,13 @@ def iter_providers(
return self._iter_type_map(type_hint, self._cached_provider_map)

def iter_processors(
self, type_hint: object | type[T]
) -> Iterable[Callable[[T], Any]]:
self, type_hint: type[T] | object
) -> Iterator[Callable[[T], Any]]:
"""Iterate over all processors of `type_hint`.
Parameters
----------
type_hint : object | Type[T]
type_hint : type[T] | object
A type or type hint for which to return processors.
Yields
Expand All @@ -510,15 +515,15 @@ def iter_processors(

# ------------------------- Instance retrieval ------------------------------

def provide(self, type_hint: object | type[T]) -> T | None:
def provide(self, type_hint: type[T] | object) -> 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 : object | Type[T]
type_hint : type[T] | object
A type or type hint for which to return a value
Returns
Expand All @@ -537,7 +542,7 @@ def process(
self,
result: Any,
*,
type_hint: object | type[T] | None = None,
type_hint: type[T] | object | None = None,
first_processor_only: bool = False,
raise_exception: bool = False,
_funcname: str = "",
Expand All @@ -552,7 +557,7 @@ def process(
----------
result : Any
The result to process
type_hint : object | type[T] | None
type_hint : type[T] | object | 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
Expand Down Expand Up @@ -854,7 +859,7 @@ def inject_processors(
self,
func: Callable[P, R],
*,
type_hint: object | type[T] | None = None,
type_hint: type[T] | object | None = None,
first_processor_only: bool = False,
raise_exception: bool = False,
) -> Callable[P, R]: ...
Expand All @@ -864,7 +869,7 @@ def inject_processors(
self,
func: Literal[None] | None = None,
*,
type_hint: object | type[T] | None = None,
type_hint: type[T] | object | None = None,
first_processor_only: bool = False,
raise_exception: bool = False,
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
Expand All @@ -873,7 +878,7 @@ def inject_processors(
self,
func: Callable[P, R] | None = None,
*,
type_hint: object | type[T] | None = None,
type_hint: type[T] | object | None = None,
first_processor_only: bool = False,
raise_exception: bool = False,
) -> Callable[[Callable[P, R]], Callable[P, R]] | Callable[P, R]:
Expand All @@ -891,7 +896,7 @@ def inject_processors(
----------
func : Callable
A function to decorate. Return hints are used to determine what to process.
type_hint : object | type[T] | None
type_hint : type[T] | object | 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)`.
Expand Down Expand Up @@ -980,8 +985,8 @@ def _build_map(self, registry: list[_RegisteredCallback]) -> _CachedMap:
return _CachedMap(all_out, subclassable_out)

def _iter_type_map(
self, hint: object | type[T], callback_map: _CachedMap
) -> Iterable[Callable]:
self, hint: type[T] | object, callback_map: _CachedMap
) -> Iterator[Callable]:
_all_types = callback_map.all
_subclassable_types = callback_map.subclassable

Expand Down Expand Up @@ -1041,14 +1046,14 @@ def _type_from_hints(hints: dict[str, Any]) -> Any:
to_register: list[_RegisteredCallback] = []
for tup in _callbacks:
callback, *rest = tup
type_: HintArg | None = None
type_: THint | 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)
type_, weight = cast(Tuple[Optional[THint], float], rest)
else: # pragma: no cover
raise ValueError(f"Invalid callback tuple: {tup!r}")

Expand Down
43 changes: 25 additions & 18 deletions tests/test_injection.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import functools
from contextlib import nullcontext
from inspect import isgeneratorfunction
from typing import ContextManager, Generator, Optional, Tuple
from typing import (
TYPE_CHECKING,
Callable,
ContextManager,
Generator,
Optional,
Sequence,
Tuple,
)
from unittest.mock import Mock

import pytest

from in_n_out import Store, _compiled, inject, inject_processors, register

if TYPE_CHECKING:
from in_n_out._type_resolution import RaiseWarnReturnIgnore

modes: Sequence["RaiseWarnReturnIgnore"] = ["raise", "warn", "return", "ignore"]


def test_injection():
@inject
Expand Down Expand Up @@ -35,7 +48,7 @@ def f(i: int) -> str:
f = inject(inject_processors(f))

with register(providers={int: lambda: 1}, processors={str: mock2}):
assert f() == "1"
assert f() == "1" # type: ignore
mock.assert_called_once_with(1)
mock2.assert_called_once_with("1")

Expand Down Expand Up @@ -90,12 +103,12 @@ def process_int(x: int) -> None:

def test_injection_with_generator():
@inject
def f(x: int) -> int:
def f(x: int) -> Generator[int, None, None]:
yield x

# setting the accessor to our local viewer
with register(providers={int: lambda: 1}):
assert tuple(f()) == (1,) # type: ignore
assert tuple(f()) == (1,)


def test_injection_without_args():
Expand All @@ -106,25 +119,19 @@ def f(): ...
assert inject(f) is f


m = ["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
...
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):
def test_injection_errors(
in_func: Callable,
on_unresolved: "RaiseWarnReturnIgnore",
on_unannotated: "RaiseWarnReturnIgnore",
) -> None:
ctx: ContextManager = nullcontext()
ctxb: ContextManager = nullcontext()
expect_same_func_back = False
Expand Down
26 changes: 13 additions & 13 deletions tests/test_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def test_set_processors(type, process, ask_type):
assert not list(ino.iter_processors(ask_type))


def test_set_processors_cleanup(test_store: ino.Store):
def test_set_processors_cleanup(test_store: ino.Store) -> None:
"""Test that we can set processors in contexts, and cleanup"""
assert not list(test_store.iter_processors(int))
mock = Mock()
Expand All @@ -59,30 +59,30 @@ def test_set_processors_cleanup(test_store: ino.Store):
assert not list(test_store.iter_processors(int))


def test_processor_decorator(test_store: ino.Store):
def test_processor_decorator(test_store: ino.Store) -> None:
"""Test the @processor decorator."""
assert not list(test_store.iter_processors(int))

@test_store.mark_processor
def processes_int(x: int): ...
def processes_int(x: int) -> None: ...

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):
def test_optional_processors(test_store: ino.Store) -> None:
"""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):
def processes_int(x: int) -> Optional[int]:
return 1

@test_store.mark_processor # these decorators are equivalent
def processes_string(x: str): ...
def processes_string(x: str) -> None: ...

# we don't have a processor guaranteed to take an int
# assert not get_processor(int)
Expand All @@ -100,9 +100,9 @@ def processes_string(x: str): ...
assert next(test_store.iter_processors(Optional[int])) is processes_int


def test_union_processors(test_store: ino.Store):
def test_union_processors(test_store: ino.Store) -> None:
@test_store.mark_processor
def processes_int_or_str(x: Union[int, str]):
def processes_int_or_str(x: Union[int, str]) -> int:
return 1

assert next(test_store.iter_processors(int)) is processes_int_or_str
Expand All @@ -112,20 +112,20 @@ def processes_int_or_str(x: Union[int, str]):
def test_unlikely_processor():
with pytest.warns(UserWarning, match="has no argument type hints"):

@ino.mark_processor
def provides_int(): ...
@ino.mark_processor # type: ignore
def provides_int() -> None: ...

with pytest.raises(ValueError, match="Processors must take at least one argument"):
ino.register(processors={int: lambda: 1})
ino.register(processors={int: lambda: 1}) # type: ignore

with pytest.raises(ValueError, match="Processors must be callable"):
ino.register(processors={int: 1})
ino.register(processors={int: 1}) # type: ignore


def test_global_register():
mock = Mock()

def f(x: int):
def f(x: int) -> None:
mock(x)

ino.register_processor(f)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def test_register_providers(type, provider, ask_type, expect):
assert not ino.provide(ask_type) # make sure context manager cleaned up


def test_provider_decorator(test_store: ino.Store):
def test_provider_decorator(test_store: ino.Store) -> None:
"""Test the @provider decorator."""
assert not test_store.provide(int)

Expand All @@ -47,7 +47,7 @@ def provides_int() -> int:
assert not test_store.provide(int)


def test_optional_providers(test_store: ino.Store):
def test_optional_providers(test_store: ino.Store) -> None:
"""Test providing & getting Optional[type]."""
assert not list(test_store.iter_providers(Optional[int]))
assert not list(test_store.iter_providers(str))
Expand Down
Loading

0 comments on commit 90f5b53

Please sign in to comment.