diff --git a/.gitignore b/.gitignore index 24c00447..f804e5b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .cache .python-version .tox +.tox-venv .eggs *.pyc *.egg-info diff --git a/README.rst b/README.rst index 16f15fe1..e64c68b2 100644 --- a/README.rst +++ b/README.rst @@ -114,9 +114,9 @@ Injection .. code-block:: python - from antidote import inject, service + from antidote import inject, injectable - @service + @injectable class Database: pass @@ -185,18 +185,18 @@ You can also retrieve the dependency by hand with :code:`world.get`: world.get[Database](Database) # with type hint, enforced when possible -Service -------- +Injectable +---------- -Services are classes for which Antidote provides an instance. It can be a singleton or not. -Scopes are also supported. Every method is injected by default, relying on annotated type -hints and markers such as :code:`inject.me()`: +Any class marked as `@injectable` can be provided by Antidote. It can be a singleton or not. +Scopes and a factory method are also supported. Every method is injected by default, relying on +annotated type hints and markers such as :code:`inject.me()`: .. code-block:: python - from antidote import service, inject + from antidote import injectable, inject - @service(singleton=False) + @injectable(singleton=False) class QueryBuilder: # methods are also injected by default def __init__(self, db: Database = inject.me()): @@ -378,9 +378,9 @@ Testing and Debugging .. code-block:: python - from antidote import service, inject + from antidote import injectable, inject - @service + @injectable class Database: pass diff --git a/bin/tox.sh b/bin/tox.sh new file mode 100755 index 00000000..8d3952e7 --- /dev/null +++ b/bin/tox.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_dir="$(dirname "$(dirname "$(readlink -f "$0")")")" +cd "$project_dir" + +if [[ "${INSIDE_DOCKER:-}" == "yes" ]]; then + venv_dir="$project_dir/.tox-venv" + if [[ ! -d "$venv_dir" ]]; then + python3.10 -m venv "$venv_dir" + source "$venv_dir/bin/activate" + pip install -r "$project_dir/requirements/dev.txt" + else + source "$venv_dir/bin/activate" + fi + tox "$@" +else + docker run \ + --user "$(id -u):$(id -g)" \ + -v "$project_dir:/antidote" \ + -w /antidote \ + -it \ + -e INSIDE_DOCKER=yes \ + quay.io/pypa/manylinux2014_x86_64:latest \ + bash -c "/antidote/bin/tox.sh $*" +fi diff --git a/docs/changelog.rst b/docs/changelog.rst index 1c1cbd4b..2661b19f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,92 @@ Most, if not all, the API is annotated with decorators such as :code:`@API.publi the given functionality can be relied upon. +1.3.0 (2022-04-26) +================== + + +Deprecation +----------- + +- :py:func:`.service` is deprecated in favor of :py:func:`.injectable` which is a drop-in + replacement. +- :py:func:`.inject` used to raise a :py:exc:`RuntimeError` when specifying + :code:`ignore_type_hints=True` and no injections were found. It now raises + :py:exc:`.NoInjectionsFoundError` +- :py:meth:`.Wiring.wire` used to return the wired class, it won't be the case anymore. + + +Features +-------- + +- Add local type hint support with :code:`type_hints_locals` argument for :py:func:`.inject`, + :py:func:`.injectable`, :py:class:`.implements` and :py:func:`.wire`. The default behavior can + be configured globally with :py:obj:`.config`. Auto-detection is done through :py:mod:`inspect` + and frame manipulation. It's mostly helpful inside tests. + + .. code-block:: python + + from __future__ import annotations + + from antidote import config, inject, injectable, world + + + def function() -> None: + @injectable + class Dummy: + pass + + @inject(type_hints_locals='auto') + def f(dummy: Dummy = inject.me()) -> Dummy: + return dummy + + assert f() is world.get(Dummy) + + + function() + + config.auto_detect_type_hints_locals = True + + + def function2() -> None: + @injectable + class Dummy: + pass + + @inject + def f(dummy: Dummy = inject.me()) -> Dummy: + return dummy + + assert f() is world.get(Dummy) + + + function2() + +- Add :code:`factory_method` to :py:func:`.injectable` (previous :py:func:`.service`) + + .. code-block:: python + + from __future__ import annotations + + from antidote import injectable + + + @injectable(factory_method='build') + class Dummy: + @classmethod + def build(cls) -> Dummy: + return cls() + +- Added :code:`ignore_type_hints` argument to :py:class:`.Wiring` and :py:func:`.wire`. +- Added :code:`type_hints_locals` and :code:`class_in_localns` argument to :py:class:`.Wiring.wire`. + + +Bug fix +------- + +- Fix :code:`Optional` detection in predicate constraints. + + 1.2.0 (2022-04-19) ================== diff --git a/docs/faq.rst b/docs/faq.rst index d7daab08..95c717ab 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -60,9 +60,9 @@ and where it should be injected: .. testcode:: why_dependency_injection - from antidote import service, inject + from antidote import injectable, inject - @service + @injectable class Database: def query(self, sql: str) -> Any: pass @@ -84,7 +84,7 @@ so you want to avoid it if possible. A simple way to do .. testcode:: why_dependency_injection - # services.py + # injectables.py from typing import Optional __db: Optional[Database] = None @@ -136,13 +136,13 @@ to do all that wiring properly. Here is the same example with Antidote: .. testcode:: why_dependency_injection - from antidote import service, inject, Constants, const + from antidote import injectable, inject, Constants, const class Config(Constants): DB_HOST = const('localhost') DB_PORT = const(5432) - @service + @injectable class Database: def __init__(self, host: str = Config.DB_HOST, @@ -267,9 +267,9 @@ Let's see how the same example looks with Antidote: # my_service.py # Antidote - from antidote import service + from antidote import injectable - @service + @injectable class MyService: pass diff --git a/docs/how_to.rst b/docs/how_to.rst index d471460d..aee79a41 100644 --- a/docs/how_to.rst +++ b/docs/how_to.rst @@ -14,9 +14,9 @@ existing dependency: .. testcode:: how_to_annotated_type_hints - from antidote import service, inject, Inject + from antidote import injectable, inject, Inject - @service + @injectable class Database: pass @@ -105,9 +105,9 @@ arguments: .. testcode:: how_to_test - from antidote import inject, service + from antidote import inject, injectable - @service + @injectable class Database: pass @@ -200,9 +200,9 @@ tree with :py:func:`.world.debug`: .. testcode:: how_to_debug - from antidote import world, service, inject + from antidote import world, injectable, inject - @service + @injectable class MyService: pass diff --git a/docs/recipes.rst b/docs/recipes.rst index 9d798fa6..88cc9053 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -225,9 +225,9 @@ Lazily calling a method requires the class to be :py:class:`.Service`. .. testcode:: recipes_lazy - from antidote import LazyMethodCall, service + from antidote import LazyMethodCall, injectable - @service + @injectable class ExampleCom: def get(url): return requests.get(f"https://example.com{url}") @@ -489,8 +489,8 @@ To use the newly created scope, use :code:`scope` parameters: .. doctest:: recipes_scope - >>> from antidote import service - >>> @service(scope=REQUEST_SCOPE) + >>> from antidote import injectable + >>> @injectable(scope=REQUEST_SCOPE) ... class Dummy: ... pass diff --git a/docs/reference.rst b/docs/reference.rst index e6f5dbf3..90860221 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -23,8 +23,8 @@ world .. doctest:: world_get - >>> from antidote import world, service - >>> @service + >>> from antidote import world, injectable + >>> @injectable ... class Dummy: ... pass >>> world.get(Dummy) @@ -57,19 +57,34 @@ world.test.override :members: + Utilities ========= + +.. automodule:: antidote._config + :members: + + .. py:data:: config + :type: antidote._config.Config + + Singleton instance of :py:class:`.Config` + + .. automodule:: antidote.utils :members: is_compiled, validated_scope, validate_injection + Lib === -Service -------- +Injectable +---------- + +.. automodule:: antidote.lib.injectable + :members: .. automodule:: antidote.service :members: service @@ -151,13 +166,18 @@ Inject .. py:function:: inject - Singleton instance of :py:class:`~.core.injection.Inject` + Singleton instance of :py:class:`~.core.injection.Injector` -.. autoclass:: antidote.core.injection.Inject +.. autoclass:: antidote.core.injection.Injector .. automethod:: __call__ .. automethod:: me - .. automethod:: get + .. py:attribute:: get + :type: antidote.core.getter.DependencyGetter + + :py:class:`.DependencyGetter` to explicit state the dependencies to be retrieved. + It follows the same API as :py:obj:`.world.get`. + .. autoclass:: antidote.core.getter.DependencyGetter :members: __call__, __getitem__ diff --git a/docs/tutorial.rst b/docs/tutorial.rst index a3115415..536fac11 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -13,13 +13,13 @@ more in depth documentation. =============== -Let's start with the basics and define a simple class that can be injected, an :py:func:`.service`: +Let's start with the basics and define a simple class that can be injected, an :py:func:`.injectable`: .. testcode:: tutorial_overview - from antidote import inject, service + from antidote import inject, injectable - @service + @injectable class Database: ... @@ -35,7 +35,7 @@ Now you don't need to provide :code:`Database` to :code:`do_stuff()` anymore! >>> load_database() -By default :py:func:`.service` declares singletons, meaning there's only one instance of +By default :py:func:`.injectable` declares singletons, meaning there's only one instance of :code:`Database`: .. doctest:: tutorial_overview @@ -97,14 +97,14 @@ Antidote will enforce the type when possible, if the provided type information i But how does Antidote work underneath ? To simplify a bit, Antidote can be summarized as single -catalog of dependencies :py:mod:`.world`. Decorators like :py:func:`.service` declares dependencies +catalog of dependencies :py:mod:`.world`. Decorators like :py:func:`.injectable` declares dependencies and :py:obj:`.inject` retrieves them:: +-----------+ +----->| world +------+ | +-----------+ | - @service @inject + @injectable @inject | | | v @@ -123,13 +123,13 @@ ways to define the dependencies to be injected. Most of them will be used in thi .. testcode:: tutorial_injection - from antidote import inject, service + from antidote import inject, injectable - @service + @injectable class Database: ... - @service + @injectable class Cache: ... @@ -211,76 +211,128 @@ The only exception is the :py:meth:`.Inject.me` marker which will provide :py:ob True -3. Services -=========== +3. Injectables +============== -A service is class that can be provided by Antidote, it's declared with :py:func:`.service`. +Any class decorated with `@injectable` can be provided by Antidote:. -.. testcode:: tutorial_services +.. testcode:: tutorial_injectables - from antidote import service + from antidote import injectable - @service + @injectable class Database: ... +.. doctest:: tutorial_injectables + :hide: + + >>> from antidote import world + >>> world.get(Database) + + >>> world.get(Database) is world.get(Database) + True + By default it's a singleton, so only one instance will exist. This behavior can be controlled with: -.. testcode:: tutorial_services +.. testcode:: tutorial_injectables - @service(singleton=False) + @injectable(singleton=False) class Database: ... -On top of declaring the dependency, :py:func:`.service` also wires the class and so injects all +.. doctest:: tutorial_injectables + :hide: + + >>> from antidote import world + >>> world.get(Database) + + >>> world.get(Database) is not world.get(Database) + True + +On top of declaring the dependency, :py:func:`.injectable` also wires the class and so injects all methods by default: -.. testcode:: tutorial_services +.. testcode:: tutorial_injectables from antidote import inject - @service + @injectable class AuthenticationService: def __init__(self, db: Database = inject.me()): self.db = db -.. doctest:: tutorial_services +.. doctest:: tutorial_injectables >>> from antidote import world >>> world.get(AuthenticationService).db -You can customize injection by applying a custom :py:func:`.inject` on methods or by specifying your -own :py:class:`.Wiring`. +You can customize injection by applying a custom :py:func:`.inject` on methods: -.. testcode:: tutorial_services +.. testcode:: tutorial_injectables + + @injectable + class AuthenticationService: + @inject({'db': Database}) + def __init__(self, db: Database): + self.db = db + +.. doctest:: tutorial_injectables + :hide: + + >>> from antidote import world + >>> world.get(AuthenticationService).db + - from __future__ import annotations + +or by specifying your +own :py:class:`.Wiring`. + +.. testcode:: tutorial_injectables from antidote import Wiring - @service + @injectable(wiring=Wiring(methods=['__init__'])) class AuthenticationService: - # Out of the box this wiring would fail as Antidote would try to retrieve the type hints - # and AuthenticationService isn't yet defined. - @inject(ignore_type_hints=True) - def __init__(self, - original: Optional[AuthenticationService] = None, - db: Database = inject.get(Database)): + def __init__(self, db: Database = inject.me()): self.db = db - @service(wiring=Wiring(methods=['__init__'])) - class AuthenticationService: - def __init__(db: Database = inject.me()): - self.db = db +.. doctest:: tutorial_injectables + :hide: + + >>> from antidote import world + >>> world.get(AuthenticationService).db + .. note:: This class wiring behavior can be used through :py:func:`.wire`, it isn't specific to - :py:func:`.service`. + :py:func:`.injectable`. + +You can also specify a factory method to control to have fine control over the instantiation: + +.. testcode:: tutorial_injectables + + from __future__ import annotations + + + @injectable(factory_method='build') + class AuthenticationService: + @classmethod + def build(cls) -> AuthenticationService: + return cls() + +.. doctest:: tutorial_injectables + :hide: + + >>> from antidote import world + >>> world.get(AuthenticationService) + + -One last point, :py:func:`.service` is best used on your own classes. If you want to register +One last point, :py:func:`.injectable` is best used on your own classes. If you want to register external classes in Antidote, you should rely on a :py:func:`~.factory.factory` instead presented in a later section. @@ -519,9 +571,9 @@ Until now, you've seen that you could still use normally injected functions: .. testcode:: tutorial_test - from antidote import service, inject + from antidote import injectable, inject - @service + @injectable class MyService: pass @@ -594,7 +646,7 @@ cannot be defined. After all you want to test your existing dependencies not cre .. doctest:: tutorial_test >>> with world.test.clone(): - ... @service + ... @injectable ... class NewService: ... pass Traceback (most recent call last): @@ -606,7 +658,7 @@ To test new dependencies, you should use :py:func:`.world.test.new` instead: .. doctest:: tutorial_test >>> with world.test.new(): - ... @service + ... @injectable ... class NewService: ... pass ... world.get(NewService) diff --git a/pyproject.toml b/pyproject.toml index 2e55633b..15d1396e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,9 @@ source = [ [tool.coverage.report] exclude_lines = [ - "pragma: no cover", - "if TYPE_CHECKING:" + "# pragma: no cover", + "if TYPE_CHECKING:$", + "^\\s*\\.\\.\\.$" ] @@ -50,14 +51,9 @@ exclude_lines = [ [tool.pyright] include = [ "src", - "tests/lib/interface/**", + "tests/lib", ] exclude = [ - "tests/core/**", - "tests/internals/**", - "tests/mypy_typing/**", - "tests/providers/**", - "tests/world/**", "tests/lib/test_constants.py", "tests/lib/test_lazy.py", "tests/lib/test_factory.py", @@ -77,4 +73,17 @@ reportSelfClsParameterName = false reportImportCycles = false # We're using our own internal APIs which we're trying to hide as much as possible reportPrivateUsage = false +# some cast / ignores are for MyPy. reportUnnecessaryTypeIgnoreComment = "warning" +reportUnnecessaryCast = "warning" + + +######## +# Mypy # +######## +[tool.mypy] +files = [ + "src", +] +python_version = "3.7" +strict = true diff --git a/requirements/docs.txt b/requirements/docs.txt index 33d7c0ec..86defbf2 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,3 +1,3 @@ -sphinx-autodoc-typehints==1.17.0 +sphinx-autodoc-typehints==1.18.1 sphinx-rtd-theme==1.0.0 -Sphinx==4.4.0 +Sphinx==4.5.0 diff --git a/src/antidote/__init__.py b/src/antidote/__init__.py index 3da62971..bc01f287 100644 --- a/src/antidote/__init__.py +++ b/src/antidote/__init__.py @@ -1,9 +1,11 @@ from . import world +from ._config import config from .constants import const, Constants from .core import Arg, From, FromArg, Get, inject, Inject, Provide, Scope, wire, Wiring from .factory import Factory, factory from .implementation import implementation from .lazy import LazyCall, LazyMethodCall +from .lib.injectable import injectable from .lib.interface import ImplementationsOf, implements, interface, QualifiedBy from .service import ABCService, Service, service from .utils import is_compiled @@ -34,9 +36,11 @@ 'const', 'constants', 'factory', + 'config', 'implementation', 'implements', 'inject', + 'injectable', 'interface', 'is_compiled', 'service', diff --git a/src/antidote/_config.py b/src/antidote/_config.py new file mode 100644 index 00000000..b9122404 --- /dev/null +++ b/src/antidote/_config.py @@ -0,0 +1,63 @@ +from typing_extensions import final + +from ._internal import API +from ._internal.utils.meta import Singleton + + +@API.public # All of the methods are public, but the only support instance is the singleton config. +@final +class Config(Singleton): + """ + Global configuration used by Antidote to (de-)activate features. + """ + + @property + def auto_detect_type_hints_locals(self) -> bool: + """ + .. versionadded:: 1.3 + + Whether :py:func:`.inject`, :py:func:`.injectable`, :py:class:`.implements` and + :py:func:`.wire` should rely on inspection to determine automatically the locals + for their argument :code:`type_hints_locals` used for + :py:func:`typing.get_type_hints`. Deactivated by default. If activated, inspection is only + used when :code:`type_hints_locals` is not specified explitely. + + It's mostly interesting during tests. The following example wouldn't work with + string annotations: + + .. doctest:: config_auto_detect_type_hints_locals + + >>> from __future__ import annotations + >>> from antidote import config, injectable, inject, world + >>> config.auto_detect_type_hints_locals = True + >>> def dummy_test(): + ... with world.test.new(): + ... @injectable + ... class Dummy: + ... pass + ... + ... @inject + ... def f(dummy: Dummy = inject.me()) -> Dummy: + ... return dummy + ... + ... return f() is world.get(Dummy) + >>> dummy_test() + True + + .. testcleanup:: config_auto_detect_type_hints_locals + + config.auto_detect_type_hints_locals = False + + """ + return bool(getattr(self, "__type_hints_locals", False)) + + @auto_detect_type_hints_locals.setter + def auto_detect_type_hints_locals(self, value: bool) -> None: + if not isinstance(value, bool): + raise TypeError(f"auto_detect_type_hints_locals must be a boolean, " + f"not a {type(value)}.") + setattr(self, "__type_hints_locals", value) + + +# API.public, but import it directly from antidote. +config = Config() diff --git a/src/antidote/_constants.py b/src/antidote/_constants.py index 3c1dfe81..1a46f30f 100644 --- a/src/antidote/_constants.py +++ b/src/antidote/_constants.py @@ -164,10 +164,11 @@ def __get__(self, raise if self.auto_cast: - value = self.type_(value) + value = cast(type, self.type_)(value) # necessary for Mypy... assert enforce_type_if_possible(value, self.type_) - return value + # See https://github.com/python/mypy/issues/11428 + return cast(Tco, value) # necessary for Mypy... @API.private diff --git a/src/antidote/_factory.py b/src/antidote/_factory.py index b2839408..9448f05e 100644 --- a/src/antidote/_factory.py +++ b/src/antidote/_factory.py @@ -11,7 +11,7 @@ from ._internal.utils import AbstractMeta, FinalImmutable from ._providers import FactoryProvider from ._providers.factory import FactoryDependency -from ._providers.service import Parameterized +from .lib.injectable._provider import Parameterized from ._utils import validate_method_parameters from .core import inject from .service import service diff --git a/src/antidote/_implementation.py b/src/antidote/_implementation.py index 059cc944..ea427e8d 100644 --- a/src/antidote/_implementation.py +++ b/src/antidote/_implementation.py @@ -11,7 +11,7 @@ T = TypeVar('T') -@API.private +# @API.private class ImplementationWrapper(Generic[P, T]): def __init__(self, wrapped: Callable[P, T], @@ -46,7 +46,7 @@ def __getattr__(self, item: str) -> object: @API.private def validate_provided_class(dependency: Hashable, *, expected: type) -> None: from ._providers.factory import FactoryDependency - from ._providers.service import Parameterized + from .lib.injectable._provider import Parameterized from ._providers.indirect import ImplementationDependency cls: object = dependency diff --git a/src/antidote/_internal/argspec.py b/src/antidote/_internal/argspec.py index 13015d08..02684227 100644 --- a/src/antidote/_internal/argspec.py +++ b/src/antidote/_internal/argspec.py @@ -1,7 +1,7 @@ from __future__ import annotations import inspect -from typing import Any, Callable, Dict, Iterator, List, Sequence, Set, Union +from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Union from typing_extensions import get_type_hints @@ -47,7 +47,8 @@ class Arguments: def from_callable(cls, func: Union[Callable[..., object], staticmethod[Any], classmethod[Any]], *, - ignore_type_hints: bool = False + ignore_type_hints: bool = False, + type_hints_locals: Optional[dict[str, object]] = None ) -> Arguments: if not (callable(func) or isinstance(func, (staticmethod, classmethod))): raise TypeError(f"func must be a callable or a static/class-method. " @@ -55,7 +56,8 @@ def from_callable(cls, return cls._build( func=func.__func__ if isinstance(func, (staticmethod, classmethod)) else func, unbound_method=is_unbound_method(func), # doing it before un-wrapping. - ignore_type_hints=ignore_type_hints + ignore_type_hints=ignore_type_hints, + type_hints_locals=type_hints_locals ) @classmethod @@ -63,7 +65,8 @@ def _build(cls, *, func: Callable[..., object], unbound_method: bool, - ignore_type_hints: bool + ignore_type_hints: bool, + type_hints_locals: Optional[dict[str, object]] ) -> Arguments: arguments: List[Argument] = [] has_var_positional = False @@ -74,8 +77,8 @@ def _build(cls, type_hints = {} extra_type_hints = {} else: - type_hints = get_type_hints(func) - extra_type_hints = get_type_hints(func, include_extras=True) + type_hints = get_type_hints(func, localns=type_hints_locals) + extra_type_hints = get_type_hints(func, localns=type_hints_locals, include_extras=True) for name, parameter in inspect.signature(func).parameters.items(): if parameter.kind is parameter.VAR_POSITIONAL: @@ -153,7 +156,7 @@ def is_unbound_method(func: Union[Callable[..., object], staticmethod[Any], clas >>> class A: ... def f(self): ... pass - >>> A.f.__qualname__ + >>> A.method.__qualname__ 'A.f' This helps us differentiate method defined in a module and those for a class. diff --git a/src/antidote/_internal/localns.py b/src/antidote/_internal/localns.py new file mode 100644 index 00000000..e8ddb751 --- /dev/null +++ b/src/antidote/_internal/localns.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import inspect +from typing import Dict, Optional, Union + +from typing_extensions import Literal + +from . import API +from .utils import Default + + +@API.private +def retrieve_or_validate_injection_locals( + type_hints_locals: Union[Dict[str, object], None, Default, Literal['auto']] +) -> Optional[Dict[str, object]]: + from .._config import config + + if type_hints_locals is Default.sentinel: + type_hints_locals = 'auto' if config.auto_detect_type_hints_locals else None + + if type_hints_locals == 'auto': + frame = inspect.currentframe() + # In theory this shouldn't be possible + if frame is None or frame.f_back is None or frame.f_back.f_back is None: # pragma: no cover + return {} + + frame = frame.f_back.f_back + first_level_locals = frame.f_locals + qualname = first_level_locals.get('__qualname__') + # If inside class namespace, trying to retrieve all locals. + if isinstance(qualname, str) and '' in qualname: + localns: dict[str, object] = {} + parts = qualname.split('.') + all_localns = [first_level_locals] + while parts.pop() != '' and frame.f_back is not None: + frame = frame.f_back + all_localns.append(frame.f_locals) + for x in reversed(all_localns): + localns.update(x) + return localns + return first_level_locals + elif not (type_hints_locals is None or isinstance(type_hints_locals, dict)): + raise TypeError(f"If specified, type_hints_locals must be None, a dict or the literal " + f"'auto', not a {type(type_hints_locals)!r}") + return type_hints_locals diff --git a/src/antidote/_internal/utils/__init__.py b/src/antidote/_internal/utils/__init__.py index da8d7903..9f33259f 100644 --- a/src/antidote/_internal/utils/__init__.py +++ b/src/antidote/_internal/utils/__init__.py @@ -20,8 +20,8 @@ UnionType = Union Im = TypeVar('Im', bound=Immutable) -T = TypeVar('T') -Tp = TypeVar('Tp', bound=type) +_T = TypeVar('_T') +_Tp = TypeVar('_Tp', bound=type) @API.private @@ -66,14 +66,14 @@ def _enforce(obj: Any, tpe: type, check: Callable[[Any, type], bool]) -> None: @API.private -def enforce_type_if_possible(obj: object, tpe: Type[T]) -> TypeGuard[T]: +def enforce_type_if_possible(obj: object, tpe: Type[_T]) -> TypeGuard[_T]: if isinstance(tpe, type): _enforce(obj, tpe, isinstance) return True @API.private -def enforce_subclass_if_possible(child: type, mother: Tp) -> TypeGuard[Tp]: +def enforce_subclass_if_possible(child: type, mother: _Tp) -> TypeGuard[_Tp]: if isinstance(mother, type) and isinstance(child, type): _enforce(child, mother, issubclass) return True @@ -97,4 +97,5 @@ def is_optional(type_hint: object) -> bool: def extract_optional_value(type_hint: object) -> Optional[object]: if is_optional(type_hint): args = cast(Any, get_args(type_hint)) - return args[0] if isinstance(None, args[1]) else args[1] + return cast(object, args[0] if isinstance(None, args[1]) else args[1]) + return None diff --git a/src/antidote/_internal/world.py b/src/antidote/_internal/world.py index b4465389..1643f39a 100644 --- a/src/antidote/_internal/world.py +++ b/src/antidote/_internal/world.py @@ -101,15 +101,15 @@ def __matmul__(self, other: SupportsRMatmul) -> LazyDependency[T]: def new_container() -> RawContainer: """ default new container in Antidote """ - from .._providers import (LazyProvider, ServiceProvider, - IndirectProvider, FactoryProvider) + from .._providers import LazyProvider, IndirectProvider, FactoryProvider from ..lib.interface._provider import InterfaceProvider + from ..lib.injectable._provider import InjectableProvider container = RawContainer() container.add_provider(FactoryProvider) container.add_provider(LazyProvider) container.add_provider(IndirectProvider) - container.add_provider(ServiceProvider) + container.add_provider(InjectableProvider) container.add_provider(InterfaceProvider) return container diff --git a/src/antidote/_providers/__init__.py b/src/antidote/_providers/__init__.py index 3bcab9d0..1454a069 100644 --- a/src/antidote/_providers/__init__.py +++ b/src/antidote/_providers/__init__.py @@ -1,8 +1,6 @@ from .factory import FactoryProvider from .indirect import IndirectProvider from .lazy import Lazy, LazyProvider -from .service import ServiceProvider from .world_test import WorldTestProvider -__all__ = ['FactoryProvider', 'IndirectProvider', 'Lazy', 'LazyProvider', - 'ServiceProvider', 'WorldTestProvider'] +__all__ = ['FactoryProvider', 'IndirectProvider', 'Lazy', 'LazyProvider', 'WorldTestProvider'] diff --git a/src/antidote/_providers/factory.py b/src/antidote/_providers/factory.py index b68dfd14..5021cef5 100644 --- a/src/antidote/_providers/factory.py +++ b/src/antidote/_providers/factory.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from typing import Callable, Dict, Hashable, List, Optional -from .service import Parameterized +from ..lib.injectable._provider import Parameterized from .._internal import API from .._internal.utils import debug_repr, FinalImmutable from ..core import (Container, DependencyDebug, DependencyValue, does_not_freeze, Provider, @@ -97,9 +97,9 @@ def maybe_provide(self, dependency: Hashable, container: Container factory.function = f.unwrapped if isinstance(dependency, Parameterized): - instance: object = factory.function(**dependency.parameters) + instance: object = factory.function(**dependency.parameters) # type: ignore else: - instance = factory.function() + instance = factory.function() # type: ignore return DependencyValue(instance, scope=factory.scope) diff --git a/src/antidote/_providers/factory.pyx b/src/antidote/_providers/factory.pyx index 88da43ca..33eed0b8 100644 --- a/src/antidote/_providers/factory.pyx +++ b/src/antidote/_providers/factory.pyx @@ -5,7 +5,7 @@ from typing import Callable, Dict, Hashable, Optional cimport cython from cpython.ref cimport PyObject -from antidote._providers.service cimport Parameterized +from antidote.lib.injectable._provider cimport Parameterized from antidote.core.container cimport (DependencyResult, FastProvider, Header, header_flag_cacheable, header_is_singleton, HeaderObject, RawContainer, Scope) from .._internal.utils import debug_repr from ..core import DependencyDebug diff --git a/src/antidote/_service.py b/src/antidote/_service.py index c3464d9c..5d8b96c8 100644 --- a/src/antidote/_service.py +++ b/src/antidote/_service.py @@ -6,8 +6,7 @@ from ._internal import API from ._internal.utils import AbstractMeta -from ._providers import ServiceProvider -from ._providers.service import Parameterized +from .lib.injectable._provider import Parameterized, InjectableProvider from ._utils import validate_method_parameters from .core import inject @@ -96,7 +95,7 @@ class ABCServiceMeta(ServiceMeta, ABCMeta): @API.private @inject def _configure_service(cls: type, - service_provider: ServiceProvider = inject.me(), + service_provider: InjectableProvider = inject.me(), conf: object = None) -> None: from .service import Service @@ -110,6 +109,6 @@ def _configure_service(cls: type, if wiring is not None: wiring.wire(cls) - validate_method_parameters(cls.__init__, conf.parameters) + validate_method_parameters(cls.__init__, conf.parameters) # type: ignore service_provider.register(cls, scope=conf.scope) diff --git a/src/antidote/core/__init__.py b/src/antidote/core/__init__.py index 8b0209f2..e113f202 100644 --- a/src/antidote/core/__init__.py +++ b/src/antidote/core/__init__.py @@ -2,7 +2,7 @@ from .container import Container, DependencyValue, Scope from .injection import Arg, DEPENDENCIES_TYPE, inject from .provider import does_not_freeze, Provider, StatelessProvider -from .typing import Source, Dependency +from .typing import Dependency, Source from .utils import DependencyDebug from .wiring import wire, Wiring, WithWiringMixin diff --git a/src/antidote/core/_injection.py b/src/antidote/core/_injection.py index 1a64e343..3a367efc 100644 --- a/src/antidote/core/_injection.py +++ b/src/antidote/core/_injection.py @@ -3,12 +3,12 @@ import collections.abc as c_abc import inspect from dataclasses import dataclass -from typing import (Any, Callable, cast, Dict, Iterable, List, Mapping, Set, # noqa: F401 +from typing import (Any, Callable, cast, Dict, Iterable, List, Mapping, Optional, Set, # noqa: F401 TYPE_CHECKING, Union) from typing_extensions import final, TypeAlias -from .exceptions import DoubleInjectionError +from .exceptions import DoubleInjectionError, NoInjectionsFoundError from .marker import Marker from .._internal import API from .._internal.argspec import Arguments @@ -52,7 +52,8 @@ def raw_inject(f: AnyF, dependencies: DEPENDENCIES_TYPE, auto_provide: AUTO_PROVIDE_TYPE, strict_validation: bool, - ignore_type_hints: bool) -> AnyF: + ignore_type_hints: bool, + type_hints_locals: Optional[Dict[str, object]]) -> AnyF: if not isinstance(ignore_type_hints, bool): raise TypeError(f"ignore_type_hints must be a boolean, not {type(ignore_type_hints)}") @@ -74,7 +75,9 @@ def raw_inject(f: AnyF, f"nor a (class/static) method") blueprint = _build_injection_blueprint( - arguments=Arguments.from_callable(f, ignore_type_hints=ignore_type_hints), + arguments=Arguments.from_callable(f, + ignore_type_hints=ignore_type_hints, + type_hints_locals=type_hints_locals), dependencies=dependencies, auto_provide=auto_provide, strict_validation=strict_validation @@ -83,7 +86,7 @@ def raw_inject(f: AnyF, # any overhead. if blueprint.is_empty(): if ignore_type_hints: - raise RuntimeError("No dependencies found while ignoring type hints!") + raise NoInjectionsFoundError(f"No dependencies found while ignoring type hints for {f}") return f wrapped_real_f = build_wrapper(blueprint=blueprint, wrapped=real_f) diff --git a/src/antidote/core/_wiring.py b/src/antidote/core/_wiring.py index 56cbeaea..5c7f00c8 100644 --- a/src/antidote/core/_wiring.py +++ b/src/antidote/core/_wiring.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import inspect -from typing import Any, Callable, cast, Dict, TypeVar, Union # noqa: F401 +from typing import Any, Callable, cast, Dict, Optional, TypeVar, Union # noqa: F401 from typing_extensions import TypeAlias -from .exceptions import DoubleInjectionError +from .exceptions import DoubleInjectionError, NoInjectionsFoundError from .injection import inject from .wiring import Methods, Wiring from .._internal import API @@ -13,14 +15,15 @@ @API.private -def wire_class(cls: C, wiring: Wiring) -> C: - if not isinstance(cls, type): - raise TypeError(f"Expecting a class, got a {type(cls)}") - +def wire_class(*, + klass: C, + wiring: Wiring, + type_hints_locals: Optional[Dict[str, object]] + ) -> C: methods: Dict[str, AnyF] = dict() if isinstance(wiring.methods, Methods): assert wiring.methods is Methods.ALL # Sanity check - for name, member in cls.__dict__.items(): + for name, member in klass.__dict__.items(): if (name in {'__call__', '__init__'} or not (name.startswith("__") and name.endswith("__"))): if (inspect.isfunction(member) @@ -29,7 +32,7 @@ def wire_class(cls: C, wiring: Wiring) -> C: else: for method_name in wiring.methods: try: - attr = cls.__dict__[method_name] + attr = klass.__dict__[method_name] except KeyError as e: raise AttributeError(method_name) from e @@ -45,13 +48,17 @@ def wire_class(cls: C, wiring: Wiring) -> C: method, dependencies=wiring.dependencies, auto_provide=wiring.auto_provide, - strict_validation=False + strict_validation=False, + ignore_type_hints=wiring.ignore_type_hints, + type_hints_locals=type_hints_locals ) except DoubleInjectionError: if wiring.raise_on_double_injection: raise + except NoInjectionsFoundError: + pass else: if injected_method is not method: # If something has changed - setattr(cls, name, injected_method) + setattr(klass, name, injected_method) - return cast(C, cls) + return klass diff --git a/src/antidote/core/annotations.py b/src/antidote/core/annotations.py index 0aa4779a..2ed960b7 100644 --- a/src/antidote/core/annotations.py +++ b/src/antidote/core/annotations.py @@ -40,10 +40,10 @@ class AntidoteAnnotation: .. doctest:: core_annotation_provide - >>> from antidote import world, inject, Inject, service + >>> from antidote import world, inject, Inject, injectable >>> from typing import Annotated ... # from typing_extensions import Annotated # Python < 3.9 - >>> @service + >>> @injectable ... class Database: ... pass >>> @inject @@ -89,7 +89,7 @@ def __init__(self, *, default: object = Default.sentinel ) -> None: - ... # pragma: no cover + ... @overload def __init__(self, @@ -98,7 +98,7 @@ def __init__(self, source: Union[Source[T], Callable[..., T], Type[CallableClass[T]]], default: object = Default.sentinel ) -> None: - ... # pragma: no cover + ... def __init__(self, __dependency: Any, diff --git a/src/antidote/core/exceptions.py b/src/antidote/core/exceptions.py index a4eaf15e..fbcaa48e 100644 --- a/src/antidote/core/exceptions.py +++ b/src/antidote/core/exceptions.py @@ -21,6 +21,17 @@ def __init__(self, func: object) -> None: super().__init__(f"Object {func} has already been injected by Antidote.") +# Inheriting RuntimeError, as it used to be one before using a custom exception. +# Do not rely on it being a RuntimeError. It will be removed in the future. +@API.public +class NoInjectionsFoundError(AntidoteError, RuntimeError): + """ + Raised when no injection could be found for a given function, even though parameters like + :code:`ignore_type_hints` were explicitly specified. Usually this implies that + :py:func:`.inject` was supposed to find injection, but just not in the type hints. + """ + + @API.public class DuplicateDependencyError(AntidoteError): """ diff --git a/src/antidote/core/getter.py b/src/antidote/core/getter.py index f3c8cd59..4f8ec807 100644 --- a/src/antidote/core/getter.py +++ b/src/antidote/core/getter.py @@ -24,13 +24,13 @@ @API.private class DependencyLoader(Protocol): def __call__(self, dependency: object, default: object) -> Any: - ... # pragma: no cover + ... @API.private class SupportsRMatmul(Protocol): def __rmatmul__(self, type_hint: object) -> object: - ... # pragma: no cover + ... @API.private # rely on world.get or inject.get @@ -57,7 +57,7 @@ def __call__(self, *, default: Union[T, Default] = Default.sentinel ) -> T: - ... # pragma: no cover + ... @overload def __call__(self, @@ -65,36 +65,32 @@ def __call__(self, *, default: Union[T, Default] = Default.sentinel ) -> T: - ... # pragma: no cover + ... @overload def __call__(self, __dependency: Type[T], *, - default: Union[T, Default] = Default.sentinel, - source: Union[Source[T], Callable[..., T], Type[CallableClass[T]]] + source: Union[Source[T], Callable[..., T], Type[CallableClass[T]]], + default: Union[T, Default] = Default.sentinel ) -> T: - ... # pragma: no cover + ... @API.public def __call__(self, - __dependency: Any, + __dependency: Union[Type[T], Dependency[T]], *, - default: Any = Default.sentinel, - source: Optional[Union[ - Source[Any], - Callable[..., Any], - Type[CallableClass[Any]] - ]] = None - ) -> Any: + source: Any = None, + default: Union[T, Default] = Default.sentinel + ) -> T: """ Retrieve the specified dependency. The interface is the same for both :py:obj:`.inject` and :py:obj:`.world`: .. doctest:: core_getter_getter - >>> from antidote import world, service, inject - >>> @service + >>> from antidote import world, injectable, inject + >>> @injectable ... class Dummy: ... pass >>> world.get(Dummy) @@ -116,10 +112,11 @@ def __call__(self, """ __dependency = cast(Any, extract_annotated_dependency(__dependency)) if source is not None: - __dependency = Get(__dependency, source=source).dependency - return self.__load(__dependency, default) + if isinstance(__dependency, Dependency): + raise TypeError("When specifying a source, the dependency must be a class") + __dependency = cast(Dependency[T], Get(__dependency, source=source).dependency) + return cast(T, self.__load(__dependency, default)) - @API.public def __getitem__(self, tpe: Type[T]) -> TypedDependencyGetter[T]: """ @@ -129,7 +126,7 @@ def __getitem__(self, tpe: Type[T]) -> TypedDependencyGetter[T]: Returns: """ - return TypedDependencyGetter(self.__enforce_type, self.__load, tpe) + return TypedDependencyGetter[T](self.__enforce_type, self.__load, tpe) @API.private # use world.get, not the class directly @@ -146,7 +143,7 @@ def __call__(self, *, default: Union[T, Default] = Default.sentinel, ) -> T: - ... # pragma: no cover + ... @overload def __call__(self, @@ -154,7 +151,7 @@ def __call__(self, default: Union[T, Default] = Default.sentinel, source: Union[Source[T], Callable[..., T], Type[CallableClass[T]]] ) -> T: - ... # pragma: no cover + ... @overload def __call__(self, @@ -162,7 +159,7 @@ def __call__(self, *, default: Union[T, Default] = Default.sentinel ) -> T: - ... # pragma: no cover + ... @overload def __call__(self, @@ -171,7 +168,7 @@ def __call__(self, default: Union[T, Default] = Default.sentinel, source: Union[Source[R], Callable[..., R], Type[CallableClass[R]]] ) -> T: - ... # pragma: no cover + ... @API.public def __call__(self, @@ -257,7 +254,7 @@ def all(self, if self.__enforce_type: assert enforce_type_if_possible(value, list) x: object - for x in value: + for x in cast(List[object], value): assert enforce_type_if_possible(x, self.__type) return cast(List[T], value) diff --git a/src/antidote/core/injection.py b/src/antidote/core/injection.py index b4928e77..e4fd6b1c 100644 --- a/src/antidote/core/injection.py +++ b/src/antidote/core/injection.py @@ -2,17 +2,17 @@ import collections.abc as c_abc import warnings -from typing import (Any, Callable, Hashable, Iterable, Mapping, Optional, - overload, - Sequence, Type, TYPE_CHECKING, TypeVar, Union) +from typing import (Any, Callable, Dict, Hashable, Iterable, Mapping, Optional, + overload, Sequence, Type, TYPE_CHECKING, TypeVar, Union) -from typing_extensions import final, TypeAlias +from typing_extensions import final, Literal, TypeAlias from .annotations import Get from .getter import DependencyGetter from .marker import InjectClassMarker, InjectFromSourceMarker, InjectImplMarker from .._internal import API -from .._internal.utils import FinalImmutable +from .._internal.localns import retrieve_or_validate_injection_locals +from .._internal.utils import Default, FinalImmutable from .._internal.utils.meta import Singleton if TYPE_CHECKING: @@ -68,7 +68,7 @@ def __init__(self, name: str, type_hint: Any, type_hint_with_extras: Any) -> Non @API.private # Use the singleton instance `inject`, not the class directly. -class Inject(Singleton): +class Injector(Singleton): """ Use :py:obj:`.inject` directly, this class is not meant to instantiated or subclassed. @@ -80,14 +80,14 @@ class Inject(Singleton): @overload def me(self) -> Any: - ... # pragma: no cover + ... @overload def me(self, *, source: Union[Source[Any], Callable[..., Any], Type[CallableClass[Any]]] ) -> Any: - ... # pragma: no cover + ... @overload def me(self, @@ -95,7 +95,7 @@ def me(self, qualified_by: Optional[object | list[object]] = None, qualified_by_one_of: Optional[list[object]] = None ) -> Any: - ... # pragma: no cover + ... @API.public def me(self, @@ -109,8 +109,8 @@ def me(self, .. doctest:: core_inject_me - >>> from antidote import inject, service - >>> @service + >>> from antidote import inject, injectable + >>> @injectable ... class MyService: ... pass >>> @inject @@ -205,9 +205,15 @@ def __call__(self, dependencies: DEPENDENCIES_TYPE = None, auto_provide: API.Deprecated[AUTO_PROVIDE_TYPE] = None, strict_validation: bool = True, - ignore_type_hints: bool = False + ignore_type_hints: bool = False, + type_hints_locals: Union[ + Dict[str, object], + Literal['auto'], + Default, + None + ] = Default.sentinel ) -> staticmethod[F]: - ... # pragma: no cover + ... @overload def __call__(self, @@ -216,9 +222,15 @@ def __call__(self, dependencies: DEPENDENCIES_TYPE = None, auto_provide: API.Deprecated[AUTO_PROVIDE_TYPE] = None, strict_validation: bool = True, - ignore_type_hints: bool = False + ignore_type_hints: bool = False, + type_hints_locals: Union[ + Dict[str, object], + Literal['auto'], + Default, + None + ] = Default.sentinel ) -> classmethod[F]: - ... # pragma: no cover + ... @overload def __call__(self, @@ -227,9 +239,15 @@ def __call__(self, dependencies: DEPENDENCIES_TYPE = None, auto_provide: API.Deprecated[AUTO_PROVIDE_TYPE] = None, strict_validation: bool = True, - ignore_type_hints: bool = False + ignore_type_hints: bool = False, + type_hints_locals: Union[ + Dict[str, object], + Literal['auto'], + Default, + None + ] = Default.sentinel ) -> F: - ... # pragma: no cover + ... @overload def __call__(self, @@ -237,9 +255,15 @@ def __call__(self, dependencies: DEPENDENCIES_TYPE = None, auto_provide: API.Deprecated[AUTO_PROVIDE_TYPE] = None, strict_validation: bool = True, - ignore_type_hints: bool = False + ignore_type_hints: bool = False, + type_hints_locals: Union[ + Dict[str, object], + Literal['auto'], + Default, + None + ] = Default.sentinel ) -> Callable[[F], F]: - ... # pragma: no cover + ... @overload def __call__(self, @@ -247,9 +271,15 @@ def __call__(self, *, auto_provide: API.Deprecated[AUTO_PROVIDE_TYPE] = None, strict_validation: bool = True, - ignore_type_hints: bool = False + ignore_type_hints: bool = False, + type_hints_locals: Union[ + Dict[str, object], + Literal['auto'], + Default, + None + ] = Default.sentinel ) -> Callable[[F], F]: - ... # pragma: no cover + ... @overload def __call__(self, @@ -257,9 +287,15 @@ def __call__(self, *, auto_provide: API.Deprecated[AUTO_PROVIDE_TYPE] = None, strict_validation: bool = True, - ignore_type_hints: bool = False + ignore_type_hints: bool = False, + type_hints_locals: Union[ + Dict[str, object], + Literal['auto'], + Default, + None + ] = Default.sentinel ) -> Callable[[F], F]: - ... # pragma: no cover + ... @API.public def __call__(self, @@ -268,7 +304,13 @@ def __call__(self, dependencies: DEPENDENCIES_TYPE = None, auto_provide: API.Deprecated[AUTO_PROVIDE_TYPE] = None, strict_validation: bool = True, - ignore_type_hints: bool = False + ignore_type_hints: bool = False, + type_hints_locals: Union[ + Dict[str, object], + Literal['auto'], + Default, + None + ] = Default.sentinel ) -> AnyF: """ Inject the dependencies into the function lazily, they are only retrieved @@ -281,11 +323,11 @@ def __call__(self, .. doctest:: core_inject - >>> from antidote import inject, service, Inject - >>> @service + >>> from antidote import inject, injectable, Inject + >>> @injectable ... class A: ... pass - >>> @service + >>> @injectable ... class B: ... pass >>> @inject @@ -304,7 +346,6 @@ def __call__(self, ... pass # a, b = , world.get('dependency') Args: - ignore_type_hints: __arg: Callable to be wrapped. Can also be used on static methods or class methods. May also be sequence of dependencies or mapping from argument name to dependencies. @@ -334,6 +375,18 @@ def __call__(self, decorated function's argumnet. For example, a key in the dependencies dict that does not match any argument would raise error. Defaults to :py:obj:`True`. + ignore_type_hints: If :py:obj:`True`, type hints will not be used at all and + :code:`type_hints_locals` will have no impact. + type_hints_locals: Local variables to use for :py:func:`typing.get_type_hints`. They + can be explicitly defined by passing a dictionary or automatically detected with + :py:mod:`inspect` and frame manipulation by specifying :code:`'auto'`. Specifying + :py:obj:`None` will deactivate the use of locals. When :code:`ignore_type_hints` is + :py:obj:`True`, this features cannot be used. The default behavior depends on the + :py:data:`.config` value of :py:attr:`~.Config.auto_detect_type_hints_locals`. If + :py:obj:`True` the default value is equivalent to specifying :code:`'auto'`, + otherwise to :py:obj:`None`. + + .. versionadded:: 1.3 Returns: The decorator to be applied or the injected function if the @@ -364,23 +417,37 @@ def __call__(self, If you rely on this behavior, wrap @inject instead. """, DeprecationWarning) + if ignore_type_hints: + if type_hints_locals is not None and type_hints_locals is not Default.sentinel: + raise TypeError(f"When ignoring type hints, type_hints_locals MUST be None " + f"or not specified at all. Got: {type_hints_locals}") + localns = None + else: + localns = retrieve_or_validate_injection_locals(type_hints_locals) + def decorate(f: AnyF) -> AnyF: return raw_inject( f, dependencies=dependencies, auto_provide=auto_provide if auto_provide is not None else False, strict_validation=strict_validation, - ignore_type_hints=ignore_type_hints + ignore_type_hints=ignore_type_hints, + type_hints_locals=localns ) return __arg and decorate(__arg) or decorate -inject = Inject() -inject.__doc__ = \ - """ - Singleton instance of :py:class:`~.core.injection.Inject` - """ +def __apply_inject(_: object) -> Injector: + return Injector() + + +# A bit unclear why this works better in PyCharm for typing. But in all cases, it looks better +# as it gets the syntax coloration of a real function. +# API.public +@__apply_inject +def inject() -> None: + ... @API.public # Function will be kept in sync with @inject, so you may use it. diff --git a/src/antidote/core/typing.py b/src/antidote/core/typing.py index 97923700..893f0178 100644 --- a/src/antidote/core/typing.py +++ b/src/antidote/core/typing.py @@ -25,7 +25,7 @@ def __antidote_dependency__(self, dependency: Type[Tct]) -> object: @API.private class CallableClass(Protocol[Tco]): def __call__(self, *args: Any, **kwargs: Any) -> Tco: - ... # pragma: no cover + ... @API.private diff --git a/src/antidote/core/wiring.py b/src/antidote/core/wiring.py index f44d9120..4b3a3946 100644 --- a/src/antidote/core/wiring.py +++ b/src/antidote/core/wiring.py @@ -1,15 +1,19 @@ from __future__ import annotations import collections.abc as c_abc +import dataclasses import enum import warnings -from typing import (Callable, FrozenSet, Iterable, Optional, overload, TypeVar, Union) +from dataclasses import dataclass +from typing import (Callable, cast, Dict, FrozenSet, Iterable, Optional, overload, TypeVar, + Union) -from typing_extensions import final +from typing_extensions import final, Literal from .injection import AUTO_PROVIDE_TYPE, DEPENDENCIES_TYPE, validate_injection from .._internal import API -from .._internal.utils import Copy, FinalImmutable +from .._internal.localns import retrieve_or_validate_injection_locals +from .._internal.utils import Copy, Default C = TypeVar('C', bound=type) _empty_set: FrozenSet[str] = frozenset() @@ -21,11 +25,12 @@ class Methods(enum.Enum): @API.public @final -class Wiring(FinalImmutable): +@dataclass(frozen=True, init=False) +class Wiring: """ Defines how a class should be wired, meaning if/how/which methods are injected. This class is intended to be used by configuration objects. If you just want to wire a - single class, consider using the class decorator :py:func:`~.wire` instead. There are + single class, consider using the class decorator :py:func:`.wire` instead. There are two purposes: - providing a default injection which can be overridden either by changing the wiring @@ -33,8 +38,7 @@ class is intended to be used by configuration objects. If you just want to wire - wiring of multiple methods with similar dependencies. Instances are immutable. If you want to change some parameters, typically defaults - defined by Antidote, you'll need to rely on :py:meth:`~.copy`. It accepts the same - arguments as :py:meth:`~.__init__` and overrides existing values. + defined by Antidote, you'll need to rely on :py:meth:`~.Wiring.copy`. .. doctest:: core_Wiring @@ -51,11 +55,12 @@ class is intended to be used by configuration objects. If you just want to wire """ - __slots__ = ('methods', 'auto_provide', 'dependencies', + __slots__ = ('methods', 'auto_provide', 'dependencies', 'ignore_type_hints', 'raise_on_double_injection') methods: Union[Methods, FrozenSet[str]] """Method names that must be injected.""" dependencies: DEPENDENCIES_TYPE + ignore_type_hints: bool auto_provide: API.Deprecated[Union[bool, FrozenSet[type], Callable[[type], bool]]] raise_on_double_injection: bool @@ -64,16 +69,23 @@ def __init__(self, methods: Union[Methods, Iterable[str]] = Methods.ALL, dependencies: DEPENDENCIES_TYPE = None, auto_provide: API.Deprecated[AUTO_PROVIDE_TYPE] = None, - raise_on_double_injection: bool = False) -> None: + raise_on_double_injection: bool = False, + ignore_type_hints: bool = False) -> None: """ Args: methods: Names of methods to be injected. If any of them is already injected, an error will be raised. Consider using :code:`attempt_methods` otherwise. - dependencies: Propagated for every method to :py:func:`~.injection.inject` + dependencies: Propagated for every method to :py:func:`.inject` auto_provide: .. deprecated:: 1.1 - Propagated for every method to :py:func:`~.injection.inject` + Propagated for every method to :py:func:`.inject` + ignore_type_hints: + If :py:obj:`True`, type hints will not be used at all and + :code:`type_hints_locals`, when calling :py:meth:`~.Wiring.wire`, will + have no impact. + + .. versionadded:: 1.3 """ if not isinstance(raise_on_double_injection, bool): @@ -106,40 +118,113 @@ def __init__(self, auto_provide = frozenset(auto_provide) validate_injection(dependencies, auto_provide) - super().__init__(methods=methods, - dependencies=dependencies, - auto_provide=auto_provide, - raise_on_double_injection=raise_on_double_injection) + if not isinstance(ignore_type_hints, bool): + raise TypeError(f"ignore_type_hints must be a boolean, not {type(ignore_type_hints)}") + + object.__setattr__(self, 'methods', methods) + object.__setattr__(self, 'dependencies', dependencies) + object.__setattr__(self, 'auto_provide', auto_provide) + object.__setattr__(self, 'raise_on_double_injection', raise_on_double_injection) + object.__setattr__(self, 'ignore_type_hints', ignore_type_hints) def copy(self, *, methods: Union[Methods, Iterable[str], Copy] = Copy.IDENTICAL, dependencies: Union[DEPENDENCIES_TYPE, Copy] = Copy.IDENTICAL, auto_provide: API.Deprecated[Union[AUTO_PROVIDE_TYPE, Copy]] = Copy.IDENTICAL, - raise_on_double_injection: Union[bool, Copy] = Copy.IDENTICAL + raise_on_double_injection: Union[bool, Copy] = Copy.IDENTICAL, + ignore_type_hints: Union[bool, Copy] = Copy.IDENTICAL ) -> Wiring: """ Copies current wiring and overrides only specified arguments. - Accepts the same arguments as :py:meth:`.__init__` + Accepts the same arguments as :py:meth:`~.Wiring.__init__` """ - return Copy.immutable(self, - methods=methods, - dependencies=dependencies, - auto_provide=auto_provide, - raise_on_double_injection=raise_on_double_injection) - + changes: dict[str, object] = {} + if methods is not Copy.IDENTICAL: + changes['methods'] = methods + if dependencies is not Copy.IDENTICAL: + changes['dependencies'] = dependencies + if auto_provide is not Copy.IDENTICAL: + changes['auto_provide'] = auto_provide + if raise_on_double_injection is not Copy.IDENTICAL: + changes['raise_on_double_injection'] = raise_on_double_injection + if ignore_type_hints is not Copy.IDENTICAL: + changes['ignore_type_hints'] = ignore_type_hints + return dataclasses.replace(self, **changes) + + @overload def wire(self, __klass: C) -> C: + ... + + @overload + def wire(self, + *, + klass: type, + type_hints_locals: Optional[dict[str, object]] = None, + class_in_localns: bool = True + ) -> None: + ... + + def wire(self, + __klass: API.Deprecated[Optional[C]] = None, + *, + klass: Optional[C] = None, + type_hints_locals: Optional[dict[str, object]] = None, + class_in_localns: Union[bool, Default] = Default.sentinel + ) -> Optional[C]: """ Used to wire a class with specified configuration. Args: - __klass: Class to wired + __klass: Class to wire. Deprecated, use :code:`klass` instead. + klass: Class to wire. + type_hints_locals: + local variables to use for :py:func:`typing.get_type_hints`. + + .. versionadded:: 1.3 + + class_in_localns: Whether to add the current class as a local variable. This + is typically helpful when the class uses itself as a type hint as during the + wiring, the class has not yet been defined in the globals/locals. The default + depends on the value of :code:`ignore_type_hints`. If ignored, the class will not + be added to the :code:`type_hints_locals`. Specifying :code:`type_hints_locals=None` + does not prevent the class to be added. + + .. versionadded:: 1.3 Returns: - The same class object with specified methods injected. + Deprecated: The same class object with specified methods injected. + It doesn't return anything with the next API. """ from ._wiring import wire_class - return wire_class(__klass, self) + if __klass is not None and not isinstance(__klass, type): + raise TypeError(f"Expecting a class, got a {type(__klass)}") + if klass is not None and not isinstance(klass, type): + raise TypeError(f"Expecting a class, got a {type(klass)}") + if klass is not None and __klass is not None: + raise ValueError("Both cls and __klass arguments cannot be used together." + "Prefer using cls as __klass is deprecated.") + + cls: C = cast(C, klass or __klass) + if __klass is None: + if type_hints_locals is not None: + if not isinstance(type_hints_locals, dict): + raise TypeError(f"type_hints_locals must be None or a dict," + f"not a {type(type_hints_locals)!r}") + if class_in_localns is Default.sentinel: + class_in_localns = not self.ignore_type_hints + elif not isinstance(class_in_localns, bool): + raise TypeError(f"class_in_localns must be a boolean if specified, " + f"not a {type(class_in_localns)!r}") + + if class_in_localns: + if self.ignore_type_hints: + raise ValueError("class_in_localns cannot be True if ignoring type hints!") + type_hints_locals = type_hints_locals or dict() + type_hints_locals.setdefault(cls.__name__, cls) + + wire_class(klass=cls, wiring=self, type_hints_locals=type_hints_locals) + return __klass @overload @@ -148,9 +233,16 @@ def wire(__klass: C, methods: Union[Methods, Iterable[str]] = Methods.ALL, dependencies: DEPENDENCIES_TYPE = None, auto_provide: API.Deprecated[AUTO_PROVIDE_TYPE] = None, - raise_on_double_injection: bool = False + raise_on_double_injection: bool = False, + ignore_type_hints: bool = False, + type_hints_locals: Union[ + Dict[str, object], + Literal['auto'], + Default, + None + ] = Default.sentinel ) -> C: - ... # pragma: no cover + ... @overload @@ -158,24 +250,52 @@ def wire(*, methods: Union[Methods, Iterable[str]] = Methods.ALL, dependencies: DEPENDENCIES_TYPE = None, auto_provide: API.Deprecated[AUTO_PROVIDE_TYPE] = None, - raise_on_double_injection: bool = False + raise_on_double_injection: bool = False, + ignore_type_hints: bool = False, + type_hints_locals: Union[ + Dict[str, object], + Literal['auto'], + Default, + None + ] = Default.sentinel ) -> Callable[[C], C]: - ... # pragma: no cover + ... @API.public -def wire(__klass: Optional[C] = None, - *, - methods: Union[Methods, Iterable[str]] = Methods.ALL, - dependencies: DEPENDENCIES_TYPE = None, - auto_provide: API.Deprecated[AUTO_PROVIDE_TYPE] = None, - raise_on_double_injection: bool = False - ) -> Union[C, Callable[[C], C]]: +def wire( + __klass: Optional[C] = None, + *, + methods: Union[Methods, Iterable[str]] = Methods.ALL, + dependencies: DEPENDENCIES_TYPE = None, + auto_provide: API.Deprecated[AUTO_PROVIDE_TYPE] = None, + raise_on_double_injection: bool = False, + ignore_type_hints: bool = False, + type_hints_locals: Union[ + Dict[str, object], + Literal['auto'], + Default, + None + ] = Default.sentinel +) -> Union[C, Callable[[C], C]]: """ Wire a class by injecting specified methods. This avoids repetition if similar dependencies need to be injected in different methods. Methods will only be wrapped if and only if Antidote may inject a dependency in it, like :py:func:`.inject`. + .. doctest:: core_wiring_wire + + >>> from antidote import wire, injectable, inject + >>> @injectable + ... class MyService: + ... pass + >>> @wire + ... class Dummy: + ... def method(self, service: MyService = inject.me()) -> MyService: + ... return service + >>> Dummy().method() + + Args: raise_on_double_injection: Whether an error should be raised if method is already injected. Defaults to :py:obj:`False`. @@ -183,9 +303,23 @@ def wire(__klass: Optional[C] = None, methods: Names of methods that must be injected. Defaults to all method dependencies: Propagated for every method to :py:func:`~.injection.inject`. auto_provide: - Propagated for every method to :py:func:`~.injection.inject`. .. deprecated:: 1.1 + Propagated for every method to :py:func:`~.injection.inject`. + ignore_type_hints: If :py:obj:`True`, type hints will not be used at all and + :code:`type_hints_locals` will have no impact. + type_hints_locals: Local variables to use for :py:func:`typing.get_type_hints`. They + can be explicitly defined by passing a dictionary or automatically detected with + :py:mod:`inspect` and frame manipulation by specifying :code:`'auto'`. Specifying + :py:obj:`None` will deactivate the use of locals. When :code:`ignore_type_hints` is + :py:obj:`True`, this features cannot be used. The default behavior depends on the + :py:data:`.config` value of :py:attr:`~.Config.auto_detect_type_hints_locals`. If + :py:obj:`True` the default value is equivalent to specifying :code:`'auto'`, + otherwise to :py:obj:`None`. + + .. versionadded:: 1.3 + + Returns: Wired class or a class decorator. @@ -194,12 +328,21 @@ def wire(__klass: Optional[C] = None, methods=methods, dependencies=dependencies, auto_provide=auto_provide, - raise_on_double_injection=raise_on_double_injection + raise_on_double_injection=raise_on_double_injection, + ignore_type_hints=ignore_type_hints ) + if wiring.ignore_type_hints: + if type_hints_locals is not None and type_hints_locals is not Default.sentinel: + raise TypeError(f"When ignoring type hints, type_hints_locals MUST be None " + f"or not specified at all. Got: {type_hints_locals}") + localns = None + else: + localns = retrieve_or_validate_injection_locals(type_hints_locals) + def wire_methods(cls: C) -> C: - from ._wiring import wire_class - return wire_class(cls, wiring) + wiring.wire(klass=cls, type_hints_locals=localns) + return cls return __klass and wire_methods(__klass) or wire_methods @@ -223,6 +366,7 @@ def with_wiring(self: W, dependencies: Union[DEPENDENCIES_TYPE, Copy] = Copy.IDENTICAL, auto_provide: API.Deprecated[Union[AUTO_PROVIDE_TYPE, Copy]] = Copy.IDENTICAL, raise_on_double_injection: Union[bool, Copy] = Copy.IDENTICAL, + ignore_type_hints: Union[bool, Copy] = Copy.IDENTICAL ) -> W: """ Accepts the same arguments as :py:class:`.Wiring`. Its only purpose is to provide @@ -240,10 +384,14 @@ def with_wiring(self: W, methods=methods, dependencies=dependencies, auto_provide=auto_provide, - raise_on_double_injection=raise_on_double_injection)) + raise_on_double_injection=raise_on_double_injection, + ignore_type_hints=ignore_type_hints + )) else: return self.copy(wiring=self.wiring.copy( methods=methods, dependencies=dependencies, auto_provide=auto_provide, - raise_on_double_injection=raise_on_double_injection)) + raise_on_double_injection=raise_on_double_injection, + ignore_type_hints=ignore_type_hints + )) diff --git a/src/antidote/factory.py b/src/antidote/factory.py index 5cc29a45..875b44d1 100644 --- a/src/antidote/factory.py +++ b/src/antidote/factory.py @@ -211,7 +211,7 @@ def factory(f: T, scope: Optional[Scope] = Scope.sentinel(), wiring: Optional[Wiring] = Wiring() ) -> T: - ... # pragma: no cover + ... @overload @@ -220,7 +220,7 @@ def factory(*, scope: Optional[Scope] = Scope.sentinel(), wiring: Optional[Wiring] = Wiring() ) -> Callable[[T], T]: - ... # pragma: no cover + ... @API.public @@ -318,7 +318,7 @@ def register_factory(func: T, if wiring is not None: try: - func = inject(func, dependencies=wiring.dependencies) + func = cast(T, inject(func, dependencies=wiring.dependencies)) except DoubleInjectionError: pass @@ -336,7 +336,7 @@ def register_factory(func: T, raise TypeError(f"The return type hint is expected to be a class, " f"not {type(output)}.") - func = service(func, singleton=True, wiring=wiring) # type: ignore + service(func, singleton=True, wiring=wiring) factory_provider.register( output=output, diff --git a/src/antidote/implementation.py b/src/antidote/implementation.py index 6a11230a..fc887ae5 100644 --- a/src/antidote/implementation.py +++ b/src/antidote/implementation.py @@ -1,12 +1,10 @@ import functools import inspect -import warnings from typing import Callable, cast, Type, TypeVar from typing_extensions import ParamSpec, Protocol from ._implementation import ImplementationWrapper, validate_provided_class -from ._internal import API from ._providers import IndirectProvider from .core import inject from .core.exceptions import DoubleInjectionError @@ -15,24 +13,23 @@ T = TypeVar('T') -@API.private +# @API.private class ImplementationProtocol(Protocol[P, T]): """ :meta private: """ def __rmatmul__(self, klass: type) -> object: # pragma: no cover - warnings.warn("Use the new `world.get(, source=)` syntax.") ... def __antidote_dependency__(self, target: Type[T]) -> object: - ... # pragma: no cover + ... def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: - ... # pragma: no cover + ... -@API.public +# @API.public def implementation(interface: type, *, permanent: bool = True @@ -129,7 +126,7 @@ def implementation(interface: type, if not inspect.isclass(interface): raise TypeError(f"interface must be a class, not {type(interface)}") - @inject + @inject # type: ignore def register(func: Callable[P, T], indirect_provider: IndirectProvider = inject.me() ) -> ImplementationProtocol[P, T]: @@ -137,7 +134,8 @@ def register(func: Callable[P, T], raise TypeError(f"{func} is not a function") try: - func = inject(func) + # for pyright + func = inject(func) # type: ignore except DoubleInjectionError: pass @@ -149,6 +147,6 @@ def impl() -> object: dependency = indirect_provider.register_implementation(interface, impl, permanent=permanent) - return ImplementationWrapper[P, T](func, dependency) # type: ignore + return ImplementationWrapper[P, T](cast(Callable[P, T], func), dependency) - return cast(Callable[[Callable[P, T]], ImplementationProtocol[P, T]], register) + return register # type: ignore diff --git a/src/antidote/lib/__init__.pxd b/src/antidote/lib/__init__.pxd new file mode 100644 index 00000000..e69de29b diff --git a/src/antidote/lib/injectable/__init__.pxd b/src/antidote/lib/injectable/__init__.pxd new file mode 100644 index 00000000..e69de29b diff --git a/src/antidote/lib/injectable/__init__.py b/src/antidote/lib/injectable/__init__.py new file mode 100644 index 00000000..a2d23094 --- /dev/null +++ b/src/antidote/lib/injectable/__init__.py @@ -0,0 +1,3 @@ +from .injectable import injectable, register_injectable_provider + +__all__ = ['register_injectable_provider', 'injectable'] diff --git a/src/antidote/lib/injectable/_internal.py b/src/antidote/lib/injectable/_internal.py new file mode 100644 index 00000000..64a7a406 --- /dev/null +++ b/src/antidote/lib/injectable/_internal.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from typing import Callable, cast, Dict, Optional, TypeVar + +from ._provider import InjectableProvider +from ..._internal import API +from ...core import inject, Scope, Wiring +from ...core.exceptions import DuplicateDependencyError + +C = TypeVar('C', bound=type) + + +@API.private +@inject +def register_injectable(*, + klass: type, + scope: Optional[Scope], + wiring: Optional[Wiring], + factory_method: Optional[str], + type_hints_locals: Optional[Dict[str, object]], + provider: InjectableProvider = inject.get(InjectableProvider) + ) -> None: + from ...service import Service + + if issubclass(klass, Service): + raise DuplicateDependencyError(f"{klass} is already defined as a dependency " + f"by inheriting {Service}") + + if wiring is not None: + wiring.wire(klass=klass, type_hints_locals=type_hints_locals) + + factory: Callable[[], type] + if factory_method is not None: + attr = getattr(klass, factory_method) + raw_attr = klass.__dict__[factory_method] + if not isinstance(raw_attr, (staticmethod, classmethod)): + raise TypeError(f"Expected a class/staticmethod for the factory_method, " + f"not {type(raw_attr)!r}") + factory = cast(Callable[[], type], attr) + else: + factory = cast(Callable[[], type], klass) # for mypy... + + provider.register(klass=klass, scope=scope, factory=factory) diff --git a/src/antidote/_providers/service.pxd b/src/antidote/lib/injectable/_provider.pxd similarity index 100% rename from src/antidote/_providers/service.pxd rename to src/antidote/lib/injectable/_provider.pxd diff --git a/src/antidote/_providers/service.py b/src/antidote/lib/injectable/_provider.py similarity index 54% rename from src/antidote/_providers/service.py rename to src/antidote/lib/injectable/_provider.py index d26859c9..9188c922 100644 --- a/src/antidote/_providers/service.py +++ b/src/antidote/lib/injectable/_provider.py @@ -1,17 +1,20 @@ from __future__ import annotations import inspect -from typing import cast, Dict, Hashable, Optional +from typing import Any, Callable, Dict, Hashable, Optional, TypeVar, Union -from .._internal import API -from .._internal.utils import debug_repr, FinalImmutable -from ..core import Container, DependencyDebug, DependencyValue, Provider, Scope +from antidote._internal import API +from antidote._internal.utils import debug_repr, FinalImmutable +from antidote.core import Container, DependencyDebug, DependencyValue, Provider, Scope +C = TypeVar('C', bound=type) + +@API.deprecated @API.private class Parameterized(FinalImmutable): __slots__ = ('wrapped', 'parameters', '_hash') - wrapped: Hashable + wrapped: Any parameters: Dict[str, object] _hash: int @@ -45,56 +48,67 @@ def __eq__(self, other: object) -> bool: @API.private -class ServiceProvider(Provider[Hashable]): +class InjectableProvider(Provider[Union[Parameterized, type]]): def __init__(self) -> None: super().__init__() - self.__services: Dict[object, Optional[Scope]] = dict() + self.__services: dict[type, tuple[Optional[Scope], Callable[..., object]]] = dict() def __repr__(self) -> str: return f"{type(self).__name__}(services={list(self.__services.items())!r})" - def exists(self, dependency: Hashable) -> bool: + def exists(self, dependency: object) -> bool: if isinstance(dependency, Parameterized): - return dependency.wrapped in self.__services + return isinstance(dependency.wrapped, type) and dependency.wrapped in self.__services return dependency in self.__services - def clone(self, keep_singletons_cache: bool) -> ServiceProvider: - p = ServiceProvider() + def clone(self, keep_singletons_cache: bool) -> InjectableProvider: + p = InjectableProvider() p.__services = self.__services.copy() return p - def maybe_debug(self, dependency: Hashable) -> Optional[DependencyDebug]: - klass = dependency.wrapped if isinstance(dependency, Parameterized) else dependency - try: - scope = self.__services[klass] - except KeyError: - return None + def debug(self, dependency: Union[Parameterized, type]) -> DependencyDebug: + if isinstance(dependency, Parameterized): + assert isinstance(dependency.wrapped, type) + klass = dependency.wrapped + else: + klass = dependency + scope, factory = self.__services[klass] return DependencyDebug(debug_repr(dependency), scope=scope, - wired=[klass]) + wired=[factory]) - def maybe_provide(self, dependency: Hashable, container: Container + def maybe_provide(self, + dependency: object, + container: Container ) -> Optional[DependencyValue]: - dep = dependency.wrapped if isinstance(dependency, Parameterized) else dependency + if isinstance(dependency, Parameterized): + if not isinstance(dependency.wrapped, type): + # Parameterized is deprecated anyway. + return None # pragma: no cover + klass: type = dependency.wrapped + elif isinstance(dependency, type): + klass = dependency + else: + return None try: - scope = self.__services[dep] + scope, factory = self.__services[klass] except KeyError: return None - klass = cast(type, dep) if isinstance(dependency, Parameterized): - instance = klass(**dependency.parameters) + instance = factory(**dependency.parameters) else: - instance = klass() + instance = factory() return DependencyValue(instance, scope=scope) def register(self, - klass: type, + klass: C, *, - scope: Optional[Scope] + scope: Optional[Scope], + factory: Optional[Callable[[], C]] = None ) -> None: assert inspect.isclass(klass) \ and (isinstance(scope, Scope) or scope is None) self._assert_not_duplicate(klass) - self.__services[klass] = scope + self.__services[klass] = scope, factory or klass diff --git a/src/antidote/_providers/service.pyx b/src/antidote/lib/injectable/_provider.pyx similarity index 68% rename from src/antidote/_providers/service.pyx rename to src/antidote/lib/injectable/_provider.pyx index 7bf21c70..180b7436 100644 --- a/src/antidote/_providers/service.pyx +++ b/src/antidote/lib/injectable/_provider.pyx @@ -5,10 +5,11 @@ from typing import Dict, Hashable cimport cython from cpython.ref cimport PyObject -from antidote.core.container cimport (DependencyResult, FastProvider, header_flag_cacheable, HeaderObject, Scope) -from .._internal.utils import debug_repr +from antidote.core.container cimport (DependencyResult, FastProvider, header_flag_cacheable, + HeaderObject, Scope, Header) +from antidote._internal.utils import debug_repr # @formatter:on -from ..core import DependencyDebug +from antidote.core import DependencyDebug cdef extern from "Python.h": PyObject *Py_True @@ -50,7 +51,7 @@ cdef class Parameterized: and self.parameters == other.parameters) # noqa @cython.final -cdef class ServiceProvider(FastProvider): +cdef class InjectableProvider(FastProvider): """ Provider managing factories. Also used to register classes directly. """ @@ -58,7 +59,7 @@ cdef class ServiceProvider(FastProvider): dict __services def __cinit__(self, dict services = None): - self.__services = services or dict() # type: Dict[Hashable, HeaderObject] + self.__services = services or dict() # type: Dict[Hashable, Injectable] def __repr__(self): return f"{type(self).__name__}(services={list(self.__services.items())!r})" @@ -68,18 +69,22 @@ cdef class ServiceProvider(FastProvider): return dependency.wrapped in self.__services return dependency in self.__services - cpdef ServiceProvider clone(self, bint keep_singletons_cache): - return ServiceProvider.__new__(ServiceProvider, self.__services.copy()) + cpdef InjectableProvider clone(self, bint keep_singletons_cache): + return InjectableProvider.__new__(InjectableProvider, self.__services.copy()) def maybe_debug(self, build: Hashable): + cdef: + Injectable injectable klass = build.wrapped if isinstance(build, Parameterized) else build try: - header = self.__services[klass] + injectable = self.__services[klass] except KeyError: return None - return DependencyDebug(debug_repr(build), - scope=header.to_scope(self._bound_container()), - wired=[klass]) + return DependencyDebug( + debug_repr(build), + scope=HeaderObject(injectable.header).to_scope(self._bound_container()), + wired=[klass] + ) cdef fast_provide(self, PyObject *dependency, @@ -96,7 +101,7 @@ cdef class ServiceProvider(FastProvider): ptr = PyDict_GetItem( self.__services, ( dependency).wrapped) if ptr: - result.header = ( ptr).header + result.header = ( ptr).header result.value = PyObject_Call( ( dependency).wrapped, empty_tuple, @@ -105,12 +110,33 @@ cdef class ServiceProvider(FastProvider): else: ptr = PyDict_GetItem( self.__services, dependency) if ptr: - result.header = ( ptr).header | header_flag_cacheable() - result.value = PyObject_CallObject(dependency, NULL) + result.header = ( ptr).header | header_flag_cacheable() + result.value = PyObject_CallObject( + ( ptr).factory, + NULL + ) - def register(self, klass: type, *, Scope scope): + def register(self, + klass: type, + *, + Scope scope, + object factory = None): + cdef: + Injectable injectable assert inspect.isclass(klass) \ and (isinstance(scope, Scope) or scope is None) with self._bound_container_ensure_not_frozen(): self._bound_container_raise_if_exists(klass) - self.__services[klass] = HeaderObject.from_scope(scope) + injectable = Injectable.__new__(Injectable) + injectable.header = HeaderObject.from_scope(scope).header + injectable.factory = factory if factory is not None else klass + self.__services[klass] = injectable + +@cython.final +cdef class Injectable: + cdef: + Header header + object factory + + def __repr__(self): + return f"Injectable(factory={self.factory})" \ No newline at end of file diff --git a/src/antidote/lib/injectable/injectable.py b/src/antidote/lib/injectable/injectable.py new file mode 100644 index 00000000..293810b5 --- /dev/null +++ b/src/antidote/lib/injectable/injectable.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +from typing import Callable, Dict, Optional, overload, TypeVar, Union + +from typing_extensions import Literal + +from ._internal import register_injectable +from ._provider import InjectableProvider +from ..._internal import API +from ..._internal.localns import retrieve_or_validate_injection_locals +from ..._internal.utils import Default +from ...core import Scope, Wiring +from ...utils import validated_scope + +__all__ = ['register_injectable_provider', 'injectable'] + +C = TypeVar('C', bound=type) + + +@API.experimental +def register_injectable_provider() -> None: + from antidote import world + world.provider(InjectableProvider) + + +@overload +def injectable(klass: C, + *, + singleton: Optional[bool] = None, + scope: Optional[Scope] = Scope.sentinel(), + wiring: Optional[Wiring] = Wiring(), + factory_method: Optional[str] = None, + type_hints_locals: Union[ + Dict[str, object], + Literal['auto'], + Default, + None + ] = Default.sentinel + ) -> C: + ... + + +@overload +def injectable(*, + singleton: Optional[bool] = None, + scope: Optional[Scope] = Scope.sentinel(), + wiring: Optional[Wiring] = Wiring(), + factory_method: Optional[str] = None, + type_hints_locals: Union[ + Dict[str, object], + Literal['auto'], + Default, + None + ] = Default.sentinel + ) -> Callable[[C], C]: + ... + + +@API.public +def injectable( + klass: Optional[C] = None, + *, + singleton: Optional[bool] = None, + scope: Optional[Scope] = Scope.sentinel(), + wiring: Optional[Wiring] = Wiring(), + factory_method: Optional[str] = None, + type_hints_locals: Union[ + Dict[str, object], + Literal['auto'], + Default, + None + ] = Default.sentinel +) -> Union[C, Callable[[C], C]]: + """ + .. versionadded:: 1.3 + + Defines the decorated class as an injectable. + + .. doctest:: lib_injectable + + >>> from antidote import injectable + >>> @injectable + ... class Dummy: + ... pass + + All methods of the classe are automatically injected by default: + + .. doctest:: lib_injectable + + >>> from antidote import world, inject + >>> @injectable + ... class MyService: + ... def __init__(self, dummy: Dummy = inject.me()): + ... self.dummy = dummy + >>> world.get(MyService).dummy + + + By default all injectables are declared as singleton, meaning only one instances will be used + during the application lifetime. But you can configure it however you need it: + + .. doctest:: lib_injectable + + >>> world.get(MyService) is world.get(MyService) + True + >>> @injectable(singleton=False) + ... class ThrowAwayService: + ... pass + >>> world.get(ThrowAwayService) is world.get(ThrowAwayService) + False + + One can also specify a :code:`factory_method` instead of relying only on :code:`__init__`. + + .. doctest:: lib_injectable + + >>> @injectable(factory_method='build') + ... class ComplexService: + ... def __init__(self, name: str, dummy: Dummy) -> None: + ... self.name = name + ... self.dummy = dummy + ... + ... @classmethod + ... def build(cls, dummy: Dummy = inject.me()) -> 'ComplexService': + ... return ComplexService('Greetings from build!', dummy) + >>> world.get(ComplexService).name + 'Greetings from build!' + + .. note:: + + If your wish to declare to register an external class to Antidote, prefer using + a factory with :py:func:`~.factory.factory`. + + Args: + klass: Class to register as a dependency. It will be instantiated only when + requested. + singleton: Whether the service is a singleton or not. A singleton is + instantiated only once. Mutually exclusive with :code:`scope`. + Defaults to :py:obj:`True` + scope: Scope of the service. Mutually exclusive with :code:`singleton`. + The scope defines if and how long the service will be cached. See + :py:class:`~.core.container.Scope`. Defaults to + :py:meth:`~.core.container.Scope.singleton`. + wiring: :py:class:`.Wiring` to be used on the class. By defaults will apply + a simple :py:func:`.inject` on all methods, so only annotated type hints are + taken into account. Can be deactivated by specifying :py:obj:`None`. + factory_method: Class or static method to use to build the class. Defaults to + :py:obj:`None`. + + .. versionadded:: 1.3 + type_hints_locals: Local variables to use for :py:func:`typing.get_type_hints`. They + can be explicitly defined by passing a dictionary or automatically detected with + :py:mod:`inspect` and frame manipulation by specifying :code:`'auto'`. Specifying + :py:obj:`None` will deactivate the use of locals. When :code:`ignore_type_hints` is + :py:obj:`True`, this features cannot be used. The default behavior depends on the + :py:data:`.config` value of :py:attr:`~.Config.auto_detect_type_hints_locals`. If + :py:obj:`True` the default value is equivalent to specifying :code:`'auto'`, + otherwise to :py:obj:`None`. + + .. versionadded:: 1.3 + + Returns: + The decorated class, unmodified, if specified or the class decorator. + + """ + scope = validated_scope(scope, singleton, default=Scope.singleton()) + if wiring is not None and not isinstance(wiring, Wiring): + raise TypeError(f"wiring must be a Wiring or None, not a {type(wiring)!r}") + if not (isinstance(factory_method, str) or factory_method is None): + raise TypeError(f"factory_method must be a class/staticmethod name or None, " + f"not a {type(factory_method)}") + + localns = retrieve_or_validate_injection_locals(type_hints_locals) + + def reg(cls: C) -> C: + if not isinstance(cls, type): + raise TypeError(f"@injectable can only be applied on classes, not {type(cls)!r}") + + register_injectable( + klass=cls, + scope=scope, + wiring=wiring, + factory_method=factory_method, + type_hints_locals=localns + ) + return cls + + return klass and reg(klass) or reg diff --git a/src/antidote/lib/interface/_internal.py b/src/antidote/lib/interface/_internal.py index 6a82fc94..505a7ee1 100644 --- a/src/antidote/lib/interface/_internal.py +++ b/src/antidote/lib/interface/_internal.py @@ -1,7 +1,7 @@ from __future__ import annotations import itertools -from typing import Any, cast, List, Optional, Type, TypeVar, Union +from typing import Any, cast, Dict, List, Optional, Type, TypeVar, Union from typing_extensions import get_type_hints, TypeAlias @@ -86,15 +86,11 @@ def create_constraints( @API.private @inject -def register_interface(__interface: C, +def register_interface(__interface: type, *, provider: InterfaceProvider = inject.get(InterfaceProvider) - ) -> C: - if not isinstance(__interface, type): - raise TypeError(f"Expected a class for the interface, got a {type(__interface)!r}") - + ) -> None: provider.register(__interface) - return cast(C, __interface) @API.private @@ -103,9 +99,10 @@ def register_implementation(*, interface: type, implementation: C, predicates: List[Union[Predicate[Weight], Predicate[NeutralWeight]]], + type_hints_locals: Optional[Dict[str, object]], provider: InterfaceProvider = inject.get(InterfaceProvider) ) -> C: - from ...service import service + from ..injectable import injectable if not isinstance(interface, type): raise TypeError(f"Expected a class for the interface, got a {type(interface)!r}") @@ -140,6 +137,6 @@ def register_implementation(*, ) try: - return cast(C, service(implementation)) + return injectable(implementation, type_hints_locals=type_hints_locals) except DuplicateDependencyError: - return cast(C, implementation) + return implementation diff --git a/src/antidote/lib/interface/interface.py b/src/antidote/lib/interface/interface.py index 83251ce7..94aff8c4 100644 --- a/src/antidote/lib/interface/interface.py +++ b/src/antidote/lib/interface/interface.py @@ -1,15 +1,17 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Callable, cast, Generic, List, Optional, Type, TypeVar +from typing import Any, Callable, cast, Dict, Generic, List, Optional, Type, TypeVar, Union -from typing_extensions import final +from typing_extensions import final, Literal from ._internal import create_constraints, register_implementation, register_interface from ._provider import InterfaceProvider, Query from .predicate import NeutralWeight, Predicate, PredicateConstraint, PredicateWeight from .qualifier import QualifiedBy from ..._internal import API +from ..._internal.localns import retrieve_or_validate_injection_locals +from ..._internal.utils import Default from ...core import Dependency, inject __all__ = ['register_interface_provider', 'interface', 'implements', 'ImplementationsOf'] @@ -132,7 +134,10 @@ def interface(klass: C) -> C: decorated interface class. """ - return register_interface(klass) + if not isinstance(klass, type): + raise TypeError(f"Expected a class for the interface, got a {type(klass)!r}") + register_interface(klass) + return klass @API.public @@ -163,18 +168,39 @@ class implements(Generic[Itf]): """ - def __init__(self, __interface: Itf) -> None: + def __init__(self, + __interface: Itf, + *, + type_hints_locals: Union[ + Dict[str, object], + Literal['auto'], + Default, + None + ] = Default.sentinel + ) -> None: """ Args: __interface: Interface class. + type_hints_locals: Local variables to use for :py:func:`typing.get_type_hints`. They + can be explicitly defined by passing a dictionary or automatically detected with + :py:mod:`inspect` and frame manipulation by specifying :code:`'auto'`. Specifying + :py:obj:`None` will deactivate the use of locals. When :code:`ignore_type_hints` is + :py:obj:`True`, this features cannot be used. The default behavior depends on the + :py:data:`.config` value of :py:attr:`~.Config.auto_detect_type_hints_locals`. If + :py:obj:`True` the default value is equivalent to specifying :code:`'auto'`, + otherwise to :py:obj:`None`. + + .. versionadded:: 1.3 """ self.__interface = __interface + self.__type_hints_locals = retrieve_or_validate_injection_locals(type_hints_locals) @API.public def __call__(self, klass: C) -> C: register_implementation( interface=self.__interface, implementation=klass, + type_hints_locals=self.__type_hints_locals, predicates=[] ) return klass @@ -215,6 +241,7 @@ def register(klass: C) -> C: register_implementation( interface=self.__interface, implementation=klass, + type_hints_locals=self.__type_hints_locals, predicates=predicates ) return klass diff --git a/src/antidote/lib/interface/predicate.py b/src/antidote/lib/interface/predicate.py index f4b5c1a6..23ff800f 100644 --- a/src/antidote/lib/interface/predicate.py +++ b/src/antidote/lib/interface/predicate.py @@ -64,7 +64,7 @@ def of_neutral(cls: Type[SelfWeight], predicate: Optional[Predicate[Any]]) -> Se Returns: Weight of the predicate or implementation. """ - ... # pragma: no cover + ... def __lt__(self: SelfWeight, other: SelfWeight) -> bool: """ @@ -74,7 +74,7 @@ def __lt__(self: SelfWeight, other: SelfWeight) -> bool: other: other will always be an instance of the current weight class. """ - ... # pragma: no cover + ... def __add__(self: SelfWeight, other: SelfWeight) -> SelfWeight: """ @@ -84,7 +84,7 @@ def __add__(self: SelfWeight, other: SelfWeight) -> SelfWeight: other: other will always be an instance of the current weight class. """ - ... # pragma: no cover + ... @API.experimental @@ -174,7 +174,7 @@ class Predicate(Protocol[WeightCo]): """ def weight(self) -> Optional[WeightCo]: - ... # pragma: no cover + ... SelfP = TypeVar('SelfP', bound=Predicate[Any]) @@ -208,7 +208,7 @@ class MergeablePredicate(Predicate[WeightCo], Protocol): @classmethod def merge(cls: Type[SelfP], a: SelfP, b: SelfP) -> SelfP: - ... # pragma: no cover + ... Pct = TypeVar('Pct', bound=Predicate[Any], contravariant=True) @@ -257,7 +257,7 @@ class PredicateConstraint(Protocol[Pct]): """ def evaluate(self, predicate: Optional[Pct]) -> bool: - ... # pragma: no cover + ... SelfPC = TypeVar('SelfPC', bound=PredicateConstraint[Any]) @@ -286,4 +286,4 @@ class MergeablePredicateConstraint(PredicateConstraint[Pct], Protocol): @classmethod def merge(cls: Type[SelfPC], a: SelfPC, b: SelfPC) -> SelfPC: - ... # pragma: no cover + ... diff --git a/src/antidote/service.py b/src/antidote/service.py index cbd38e58..c4cfbc0c 100644 --- a/src/antidote/service.py +++ b/src/antidote/service.py @@ -20,7 +20,7 @@ class Service(metaclass=ServiceMeta, abstract=True): """ .. deprecated:: 1.1 - Use :py:func:`~.service.service` instead. + Use :py:func:`.injectable` instead. .. note:: @@ -118,7 +118,7 @@ class Conf(FinalImmutable, WithWiringMixin): @property def singleton(self) -> bool: - warnings.warn("Service class is deprecated, use @service decorator instead.", + warnings.warn("Service class is deprecated, use @injectable decorator instead.", DeprecationWarning) return self.scope is Scope.singleton() @@ -142,7 +142,7 @@ def __init__(self, :py:class:`~.core.container.Scope`. Defaults to :py:meth:`~.core.container.Scope.singleton`. """ - warnings.warn("Service class is deprecated, use @service decorator instead.", + warnings.warn("Service class is deprecated, use @injectable decorator instead.", DeprecationWarning) if not (wiring is None or isinstance(wiring, Wiring)): raise TypeError(f"wiring must be a Wiring or None, not {type(wiring)}") @@ -166,7 +166,7 @@ def copy(self, Copies current configuration and overrides only specified arguments. Accepts the same arguments as :py:meth:`.__init__` """ - warnings.warn("Service class is deprecated, use @service decorator instead.", + warnings.warn("Service class is deprecated, use @injectable decorator instead.", DeprecationWarning) if not (singleton is Copy.IDENTICAL or scope is Copy.IDENTICAL): raise TypeError("Use either singleton or scope argument, not both.") @@ -182,11 +182,11 @@ def copy(self, """ def __init__(self) -> None: - warnings.warn("Service class is deprecated, use @service decorator instead.", + warnings.warn("Service class is deprecated, use @injectable decorator instead.", DeprecationWarning) def __init_subclass__(cls, **kwargs: Any) -> None: - warnings.warn("Service class is deprecated, use @service decorator instead.", + warnings.warn("Service class is deprecated, use @injectable decorator instead.", DeprecationWarning) super().__init_subclass__(**kwargs) @@ -195,7 +195,7 @@ def __init_subclass__(cls, **kwargs: Any) -> None: class ABCService(Service, metaclass=ABCServiceMeta, abstract=True): """ .. deprecated:: 1.1 - Use :py:func:`~.service.service` instead. + Use :py:func:`.injectable` instead. This class only purpose is to facilitate the use of a abstract parent class, relying on :py:class:`abc.ABC`, with :py:class:`.Service`. @@ -222,18 +222,18 @@ def service(klass: C, *, singleton: Optional[bool] = None, scope: Optional[Scope] = Scope.sentinel(), - wiring: Optional[Wiring] = Wiring() + wiring: Optional[Wiring] = Wiring(), ) -> C: - ... # pragma: no cover + ... @overload def service(*, singleton: Optional[bool] = None, scope: Optional[Scope] = Scope.sentinel(), - wiring: Optional[Wiring] = Wiring() + wiring: Optional[Wiring] = Wiring(), ) -> Callable[[C], C]: - ... # pragma: no cover + ... @API.public @@ -241,9 +241,12 @@ def service(klass: Optional[C] = None, *, singleton: Optional[bool] = None, scope: Optional[Scope] = Scope.sentinel(), - wiring: Optional[Wiring] = Wiring() + wiring: Optional[Wiring] = Wiring(), ) -> Union[C, Callable[[C], C]]: """ + .. deprecated:: 1.3 + Use :py:func:`.injectable` instead. + Defines the decorated class as a service. .. doctest:: service_decorator diff --git a/src/antidote/world/test/_methods.py b/src/antidote/world/test/_methods.py index c86cb711..f3f7b1fd 100644 --- a/src/antidote/world/test/_methods.py +++ b/src/antidote/world/test/_methods.py @@ -129,12 +129,12 @@ def build_new_container(existing: RawContainer) -> RawContainer: @overload def singleton(dependency: Hashable, value: object) -> None: - ... # pragma: no cover + ... @overload def singleton(dependency: Dict[Hashable, object]) -> None: - ... # pragma: no cover + ... @API.public diff --git a/src/antidote/world/test/_override.py b/src/antidote/world/test/_override.py index 9ce90277..8922d783 100644 --- a/src/antidote/world/test/_override.py +++ b/src/antidote/world/test/_override.py @@ -11,12 +11,12 @@ @overload def singleton(dependency: Hashable, value: object) -> None: - ... # pragma: no cover + ... @overload def singleton(dependency: Dict[Hashable, object]) -> None: - ... # pragma: no cover + ... @API.public diff --git a/tests/conftest.py b/tests/conftest.py index 30d6013b..0dadca72 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,8 @@ import pytest -from antidote import is_compiled +from antidote import config, is_compiled + +config.auto_detect_type_hints_locals = True def pytest_runtest_setup(item): diff --git a/tests/core/test_annotations_get.py b/tests/core/test_annotations_get.py index 10ee4c6e..35998d49 100644 --- a/tests/core/test_annotations_get.py +++ b/tests/core/test_annotations_get.py @@ -3,7 +3,7 @@ import pytest from typing_extensions import Annotated -from antidote import factory, Get, inject, service, world +from antidote import factory, Get, inject, injectable, world @pytest.fixture(autouse=True) @@ -13,7 +13,7 @@ def new_world(): def test_injectable(): - @service + @injectable class Dummy: pass @@ -30,7 +30,7 @@ def g(x: Annotated[Dummy, Get(Dummy)]): def test_nested_get(): - @service + @injectable class Dummy: pass diff --git a/tests/core/test_getter.py b/tests/core/test_getter.py new file mode 100644 index 00000000..d786aa95 --- /dev/null +++ b/tests/core/test_getter.py @@ -0,0 +1,14 @@ +import pytest + +from antidote.core import Dependency +from antidote.core.getter import DependencyGetter + + +class Dummy(Dependency[object]): + pass + + +def test_dependency_cannot_have_source(): + getter = DependencyGetter.raw(lambda dependency, default: dependency) + with pytest.raises(TypeError): + getter(Dummy(), source=lambda: object()) diff --git a/tests/core/test_injection.py b/tests/core/test_injection.py index 3216ca32..5e7a04e7 100644 --- a/tests/core/test_injection.py +++ b/tests/core/test_injection.py @@ -6,6 +6,7 @@ from antidote import From, FromArg, Get, world from antidote.core.annotations import Provide +from antidote.core.exceptions import NoInjectionsFoundError from antidote.core.injection import inject, validate_injection from antidote.exceptions import DependencyNotFoundError, DoubleInjectionError @@ -952,3 +953,8 @@ def test_no_injection_error_when_ignoring_type_hints(): @inject(ignore_type_hints=True) def f(): pass + + with pytest.raises(NoInjectionsFoundError, match="(?i)No dependencies found.*"): + @inject(ignore_type_hints=True) + def g(): + pass diff --git a/tests/core/test_injection_localns.py b/tests/core/test_injection_localns.py new file mode 100644 index 00000000..d8499a45 --- /dev/null +++ b/tests/core/test_injection_localns.py @@ -0,0 +1,190 @@ +from __future__ import annotations + +from typing import Iterator + +import pytest + +from antidote import config, inject, wire, world + + +@pytest.fixture(autouse=True) +def setup_world() -> Iterator[None]: + with world.test.empty(): + yield + + +def test_locals(): + class Dummy: + pass + + dummy = Dummy() + world.test.singleton(Dummy, dummy) + + @inject + def f(x: Dummy = inject.me()) -> Dummy: + return x + + assert f() is dummy + + +def test_local_class(): + class Dummy: + pass + + dummy = Dummy() + world.test.singleton(Dummy, dummy) + + class Service: + @inject + def method(self, x: Dummy = inject.me()) -> Dummy: + return x + + assert Service().method() is dummy + + @wire + class Service2: + def method(self, x: Dummy = inject.me()) -> Dummy: + return x + + assert Service2().method() is dummy + + +def test_local_nested_class(): + class Dummy: + pass + + dummy = Dummy() + world.test.singleton(Dummy, dummy) + + class Service1: + class Service2: + class Service3: + @inject + def method(self, x: Dummy = inject.me()) -> Dummy: + return x + + @wire + class Service3b: + def method(self, x: Dummy = inject.me()) -> Dummy: + return x + + assert Service1.Service2.Service3().method() is dummy + assert Service1.Service2.Service3b().method() is dummy + + +def test_invalid_locals(): + with pytest.raises(TypeError, match="(?i).*type_hints_locals.*"): + inject(type_hints_locals=object()) # type: ignore + + with pytest.raises(TypeError, match="(?i).*type_hints_locals.*"): + wire(type_hints_locals=object()) # type: ignore + + +def test_explicit_locals(): + class Dummy: + pass + + class AlternateDummy: + pass + + alternate_dummy = AlternateDummy() + world.test.singleton(AlternateDummy, alternate_dummy) + + @inject(type_hints_locals={'Dummy': AlternateDummy}) + def f(x: Dummy = inject.me()) -> Dummy: + return x + + assert f() is alternate_dummy + + @wire(type_hints_locals={'Dummy': AlternateDummy}) + class Service: + def method(self, x: Dummy = inject.me()) -> Dummy: + return x + + assert Service().method() is alternate_dummy + + +def test_no_locals(): + class Dummy: + pass + + dummy = Dummy() + world.test.singleton(Dummy, dummy) + + with pytest.raises(NameError, match="(?i).*Dummy.*"): + @inject(type_hints_locals=None) + def f(x: Dummy = inject.me()) -> Dummy: + return x + + with pytest.raises(NameError, match="(?i).*Dummy.*"): + @inject(type_hints_locals={}) + def g(x: Dummy = inject.me()) -> Dummy: + return x + + with pytest.raises(NameError, match="(?i).*Dummy.*"): + @wire(type_hints_locals=None) + class F: + def method(self, x: Dummy = inject.me()) -> Dummy: + return x + + with pytest.raises(NameError, match="(?i).*Dummy.*"): + @wire(type_hints_locals={}) + class G: + def method(self, x: Dummy = inject.me()) -> Dummy: + return x + + +def test_no_type_hints(): + class Dummy: + pass + + dummy = Dummy() + world.test.singleton(Dummy, dummy) + + with pytest.raises(TypeError, match=".*@inject.me.*"): + @inject(ignore_type_hints=True) + def f(x: Dummy = inject.me()) -> Dummy: + return x + + with pytest.raises(TypeError, match=".*@inject.me.*"): + @wire(ignore_type_hints=True) + class F: + def method(self, x: Dummy = inject.me()) -> Dummy: + return x + + with pytest.raises(TypeError, match=".*type_hints_locals.*"): + inject(type_hints_locals={}, ignore_type_hints=True) + + with pytest.raises(TypeError, match=".*type_hints_locals.*"): + wire(type_hints_locals={}, ignore_type_hints=True) + + @inject(ignore_type_hints=True) + def g(x: Dummy = inject.get(Dummy)) -> Dummy: + return x + + # Sanity check + assert g() is world.get(Dummy) + + +def test_config_not_activated(): + config.auto_detect_type_hints_locals = False + try: + class Dummy: + pass + + dummy = Dummy() + world.test.singleton(Dummy, dummy) + + with pytest.raises(NameError, match=".*Dummy.*"): + @inject + def f(x: Dummy = inject.me()) -> Dummy: + return x + + with pytest.raises(NameError, match=".*Dummy.*"): + @wire + class Service: + def method(self, x: Dummy = inject.me()) -> Dummy: + return x + + finally: + config.auto_detect_type_hints_locals = True diff --git a/tests/core/test_injection_markers.py b/tests/core/test_injection_markers.py index 8c2fd0ca..0d8a9e64 100644 --- a/tests/core/test_injection_markers.py +++ b/tests/core/test_injection_markers.py @@ -3,7 +3,7 @@ import pytest -from antidote import factory, inject, QualifiedBy, service, world +from antidote import factory, inject, QualifiedBy, injectable, world from antidote.core.marker import Marker from antidote.exceptions import DependencyNotFoundError @@ -14,12 +14,8 @@ def setup_world() -> Iterator[None]: yield -class GlobalService: - pass - - def test_marker_me(): - @service + @injectable class MyService: pass @@ -117,7 +113,7 @@ def f2(x: MyService = inject.me(QualifiedBy(object()), source=create_service)): def test_marker_get(): - @service + @injectable class MyService: pass @@ -184,32 +180,34 @@ def test(test=CustomMarker()): def test_marker_me_optional(): - service(GlobalService) + @injectable + class MyService: + pass @inject - def f(my_service: Optional[GlobalService] = inject.me()): + def f(my_service: Optional[MyService] = inject.me()): return my_service - assert f() is world.get[GlobalService]() + assert f() is world.get[MyService]() with world.test.empty(): assert f() is None @inject - def f2(my_service: Union[GlobalService, None] = inject.me()): + def f2(my_service: Union[MyService, None] = inject.me()): return my_service - assert f2() is world.get[GlobalService]() + assert f2() is world.get[MyService]() with world.test.empty(): assert f2() is None if sys.version_info >= (3, 10): @inject - def f3(my_service: 'GlobalService | None' = inject.me()): + def f3(my_service: 'MyService | None' = inject.me()): return my_service - assert f3() is world.get[GlobalService]() + assert f3() is world.get[MyService]() with world.test.empty(): assert f3() is None diff --git a/tests/core/test_wiring.py b/tests/core/test_wiring.py index 0077b4dd..f2c50c77 100644 --- a/tests/core/test_wiring.py +++ b/tests/core/test_wiring.py @@ -1,4 +1,6 @@ -from typing import TYPE_CHECKING +from __future__ import annotations + +from dataclasses import dataclass import pytest @@ -6,9 +8,6 @@ from antidote.core.exceptions import DoubleInjectionError from antidote.core.wiring import Wiring, WithWiringMixin -if TYPE_CHECKING: - pass - class MyService: pass @@ -19,11 +18,18 @@ class AnotherService: @pytest.fixture(params=['Wiring', 'wire']) -def wire(request): +def wire_(request): kind = request.param if kind == 'Wiring': def wire(**kwargs): - return Wiring(**kwargs).wire + type_hints_locals = kwargs.pop('type_hints_locals', None) + wiring = Wiring(**kwargs) + + def decorator(cls: type) -> type: + wiring.wire(klass=cls, type_hints_locals=type_hints_locals) + return cls + + return decorator return wire else: @@ -47,8 +53,8 @@ def new_world(): dict(dependencies=('x', 'y')), dict(dependencies=dict(x='x', y='y')), ]) -def test_no_strict_validation(wire, kwargs): - @wire(**kwargs) +def test_no_strict_validation(wire_, kwargs): + @wire_(**kwargs) class A: def f(self, x): return x @@ -61,8 +67,8 @@ def g(self, x, y): assert a.g() == (world.get("x"), world.get("y")) -def test_no_strict_validation_auto_provide(wire): - @wire(auto_provide=[MyService, AnotherService]) +def test_no_strict_validation_auto_provide(wire_): + @wire_(auto_provide=[MyService, AnotherService]) class A: def f(self, x: MyService): return x @@ -75,8 +81,8 @@ def g(self, x: MyService, y: AnotherService): assert a.g() == (world.get(MyService), world.get(AnotherService)) -def test_subclass_classmethod(wire): - @wire(auto_provide=True) +def test_subclass_classmethod(wire_): + @wire_(auto_provide=True) class Dummy: @classmethod def cls_method(cls, x: MyService): @@ -90,17 +96,19 @@ class SubDummy(Dummy): assert (SubDummy, world.get(MyService)) == SubDummy.cls_method() -@pytest.mark.parametrize('kwargs, expectation', [ - (dict(methods=object()), pytest.raises(TypeError, match=".*method.*")), - (dict(methods=[object()]), pytest.raises(TypeError, match="(?i).*method.*")), - (dict(dependencies=object()), pytest.raises(TypeError, match=".*dependencies.*")), - (dict(auto_provide=object()), pytest.raises(TypeError, match=".*auto_provide.*")), - (dict(raise_on_double_injection=object()), - pytest.raises(TypeError, match=".*raise_on_double_injection.*")), -]) -def test_validation(wire, kwargs, expectation): - with expectation: - wire(**kwargs) +@pytest.mark.parametrize('arg', + ['methods', 'dependencies', 'auto_provide', 'raise_on_double_injection', + 'ignore_type_hints', 'type_hints_locals']) +def test_invalid_arguments(wire_, arg: str) -> None: + with pytest.raises(TypeError, match=".*" + arg + ".*"): + @wire_(**{arg: object()}) # type: ignore + class Dummy: + pass + + +def test_invalid_class(wire_): + with pytest.raises(TypeError): + wire_()(object()) def test_iterable(): @@ -113,8 +121,8 @@ def test_iterable(): assert w.auto_provide == {MyService} -def test_default_all_methods(wire): - @wire() +def test_default_all_methods(wire_): + @wire_() class A: def __init__(self, x: Provide[MyService]): self.x = x @@ -170,8 +178,8 @@ def __static(x: Provide[MyService]): assert a._A__static() is dummy -def test_methods(wire): - @wire(methods=['f'], dependencies={'x': MyService}) +def test_methods(wire_): + @wire_(methods=['f'], dependencies={'x': MyService}) class A: def f(self, x): return x @@ -184,13 +192,13 @@ def g(self, x): A().g() with pytest.raises(AttributeError): - @wire(methods=['f']) + @wire_(methods=['f']) class A: pass -def test_double_injection(wire): - @wire(methods=['f']) +def test_double_injection(wire_): + @wire_(methods=['f']) class A: @inject(auto_provide=True) # auto_provide to force injection def f(self, x: MyService): @@ -199,13 +207,13 @@ def f(self, x: MyService): assert A().f() is world.get(MyService) with pytest.raises(DoubleInjectionError): - @wire(methods=['f'], raise_on_double_injection=True) + @wire_(methods=['f'], raise_on_double_injection=True) class B: @inject(auto_provide=True) # auto_provide to force injection def f(self, x: MyService): return x - @wire() + @wire_() class C: @inject(auto_provide=True) # auto_provide to force injection def f(self, x: MyService): @@ -214,19 +222,22 @@ def f(self, x: MyService): assert C().f() is world.get(MyService) with pytest.raises(DoubleInjectionError): - @wire(raise_on_double_injection=True) + @wire_(raise_on_double_injection=True) class D: @inject(auto_provide=True) # auto_provide to force injection def f(self, x: MyService): return x -def test_invalid_methods(wire): +def test_invalid_methods(wire_): with pytest.raises(TypeError, match='.*not_a_method.*'): - @wire(methods=['not_a_method']) + @wire_(methods=['not_a_method']) class Dummy: not_a_method = 1 + with pytest.raises(TypeError, match='.*methods.*'): + wire_(methods=[object()]) + @pytest.fixture(params=['method', 'classmethod', 'staticmethod']) def wired_method_builder(request): @@ -236,63 +247,70 @@ def build(wire, *, annotation=object): if kind == 'method': @wire class A: - def f(self, x: annotation): + def f(self, x): return x + + f.__annotations__['x'] = annotation + elif kind == 'classmethod': @wire class A: - @classmethod def f(cls, x: annotation): return x + + f.__annotations__['x'] = annotation + f = classmethod(f) else: @wire class A: - @staticmethod def f(x: annotation): return x + + f.__annotations__['x'] = annotation + f = staticmethod(f) return A().f return build -def test_use_inject_annotation(wire, wired_method_builder): - f = wired_method_builder(wire()) +def test_use_inject_annotation(wire_, wired_method_builder): + f = wired_method_builder(wire_()) with pytest.raises(TypeError): f() - f = wired_method_builder(wire(), annotation=Provide[MyService]) + f = wired_method_builder(wire_(), annotation=Provide[MyService]) assert f() is world.get(MyService) -def test_dependencies(wire, wired_method_builder): - f = wired_method_builder(wire(methods=['f'], dependencies=('y',))) +def test_dependencies(wire_, wired_method_builder): + f = wired_method_builder(wire_(methods=['f'], dependencies=('y',))) assert f() is world.get('y') - f = wired_method_builder(wire(methods=['f'], dependencies=dict(x='z'))) + f = wired_method_builder(wire_(methods=['f'], dependencies=dict(x='z'))) assert f() is world.get('z') - f = wired_method_builder(wire(methods=['f'], dependencies=dict(y='z'))) + f = wired_method_builder(wire_(methods=['f'], dependencies=dict(y='z'))) with pytest.raises(TypeError): f() - f = wired_method_builder(wire(methods=['f'], dependencies=lambda arg: arg.name * 2)) + f = wired_method_builder(wire_(methods=['f'], dependencies=lambda arg: arg.name * 2)) assert f() is world.get('xx') @pytest.mark.parametrize('annotation', [object, MyService]) -def test_wiring_auto_provide(wire, wired_method_builder, annotation): - f = wired_method_builder(wire(methods=['f']), +def test_wiring_auto_provide(wire_, wired_method_builder, annotation): + f = wired_method_builder(wire_(methods=['f']), annotation=annotation) with pytest.raises(TypeError): f() # Boolean - f = wired_method_builder(wire(methods=['f'], auto_provide=False), + f = wired_method_builder(wire_(methods=['f'], auto_provide=False), annotation=annotation) with pytest.raises(TypeError): f() - f = wired_method_builder(wire(methods=['f'], auto_provide=True), + f = wired_method_builder(wire_(methods=['f'], auto_provide=True), annotation=annotation) if annotation is MyService: assert f() is world.get(MyService) @@ -301,7 +319,7 @@ def test_wiring_auto_provide(wire, wired_method_builder, annotation): f() # List - f = wired_method_builder(wire(methods=['f'], auto_provide=[MyService]), + f = wired_method_builder(wire_(methods=['f'], auto_provide=[MyService]), annotation=annotation) if annotation is MyService: assert f() is world.get(MyService) @@ -312,14 +330,14 @@ def test_wiring_auto_provide(wire, wired_method_builder, annotation): class Unknown: pass - f = wired_method_builder(wire(methods=['f'], auto_provide=[Unknown]), + f = wired_method_builder(wire_(methods=['f'], auto_provide=[Unknown]), annotation=annotation) with pytest.raises(TypeError): f() # Function - f = wired_method_builder(wire(methods=['f'], - auto_provide=lambda cls: issubclass(cls, MyService)), + f = wired_method_builder(wire_(methods=['f'], + auto_provide=lambda cls: issubclass(cls, MyService)), annotation=annotation) if annotation is MyService: assert f() is world.get(MyService) @@ -327,15 +345,15 @@ class Unknown: with pytest.raises(TypeError): f() - f = wired_method_builder(wire(methods=['f'], auto_provide=lambda cls: False), + f = wired_method_builder(wire_(methods=['f'], auto_provide=lambda cls: False), annotation=annotation) with pytest.raises(TypeError): f() -def test_complex_wiring(wire): - @wire(auto_provide=True, - methods=['g']) +def test_complex_wiring(wire_): + @wire_(auto_provide=True, + methods=['g']) class A: def f(self, x: MyService): return x @@ -349,9 +367,9 @@ def g(self, x: MyService): assert A().g() == world.get(MyService) -def test_class_static_methods(wire): - @wire(methods=['static', 'klass'], - auto_provide=True) # auto_provide to force injection +def test_class_static_methods(wire_): + @wire_(methods=['static', 'klass'], + auto_provide=True) # auto_provide to force injection class Dummy: @staticmethod def static(x: MyService): @@ -404,3 +422,73 @@ def test_with_wiring(kwargs): copy = DummyConf().with_wiring(**kwargs) for key, value in kwargs.items(): assert getattr(copy.wiring, key) == value + + +def test_new_old_wiring_wire(): + wiring = Wiring() + + class Dummy: + pass + + with pytest.raises(TypeError, match=".*class.*"): + wiring.wire(object()) + + with pytest.raises(TypeError, match=".*class.*"): + wiring.wire(klass=object()) + + with pytest.raises(ValueError, match=".*cls.*__klass.*together.*"): + wiring.wire(Dummy, klass=Dummy) + + +@pytest.mark.parametrize('type_hints_locals', [None, dict()]) +def test_class_in_localns(type_hints_locals): + wiring = Wiring() + + @dataclass + class Dummy: + service: MyService + + @classmethod + def create(cls, service: MyService = inject.me()) -> Dummy: + return Dummy(service=service) + + with pytest.raises(NameError, match="Dummy"): + wiring.wire(klass=Dummy, type_hints_locals=type_hints_locals, class_in_localns=False) + + wiring.wire(klass=Dummy, type_hints_locals=type_hints_locals, class_in_localns=True) + assert Dummy.create().service is world.get(MyService) + + +def test_default_class_in_localns(wire_): + @wire_() + @dataclass + class Dummy: + service: MyService + + @classmethod + def create(cls, service: MyService = inject.me()) -> Dummy: + return Dummy(service=service) + + assert Dummy.create().service is world.get(MyService) + + @wire_(ignore_type_hints=True) + @dataclass + class Dummy2: + service: MyService + + @classmethod + def create(cls, service: MyService = inject.get(MyService)) -> Dummy: + return Dummy(service=service) + + assert Dummy2.create().service is world.get(MyService) + + +def test_invalid_class_in_localns(): + class Dummy: + pass + + with pytest.raises(TypeError, match="class_in_localns"): + Wiring().wire(klass=Dummy, class_in_localns=object()) + + with pytest.raises(ValueError, match="class_in_localns"): + Wiring(ignore_type_hints=True).wire(klass=Dummy, class_in_localns=True) diff --git a/tests/lib/injectable/__init__.py b/tests/lib/injectable/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/lib/injectable/test_injectable.py b/tests/lib/injectable/test_injectable.py new file mode 100644 index 00000000..fda65611 --- /dev/null +++ b/tests/lib/injectable/test_injectable.py @@ -0,0 +1,227 @@ +# pyright: reportUnusedClass=false +from __future__ import annotations + +from typing import Iterator + +import pytest + +from antidote import inject, Inject, injectable, Service, Wiring, world +from antidote.core.exceptions import DuplicateDependencyError +from antidote.lib.injectable import register_injectable_provider + + +@pytest.fixture(autouse=True) +def setup_world() -> Iterator[None]: + with world.test.empty(): + register_injectable_provider() + yield + + +def test_simple() -> None: + @injectable + class Dummy: + pass + + dummy = world.get(Dummy) + assert isinstance(dummy, Dummy) + # Singleton by default + assert world.get(Dummy) is dummy + + @inject + def f(x: Dummy = inject.me()) -> Dummy: + return x + + assert f() is dummy + + @inject + def f2(x: Dummy = inject.get(Dummy)) -> Dummy: + return x + + assert f2() is dummy + + +def test_singleton() -> None: + @injectable(singleton=True) + class Single: + pass + + @injectable(singleton=False) + class Consumable: + pass + + assert isinstance(world.get(Single), Single) + assert world.get(Single) is world.get(Single) + + assert isinstance(world.get(Consumable), Consumable) + assert world.get(Consumable) is not world.get(Consumable) + + +def test_scope() -> None: + scope = world.scopes.new(name="dummy") + + @injectable(scope=scope) + class Scoped: + pass + + scoped = world.get(Scoped) + assert isinstance(scoped, Scoped) + assert world.get(Scoped) is scoped + + world.scopes.reset(scope) + + new_scoped = world.get(Scoped) + assert isinstance(new_scoped, Scoped) + assert new_scoped is not scoped + + +def test_default_wiring() -> None: + @injectable + class Dummy: + pass + + @injectable + class WithWiring: + def __init__(self, x: Dummy = inject.me()) -> None: + self.dummy = x + + def method(self, x: Dummy = inject.me()) -> Dummy: + return x + + @inject + def injected_method(self, x: Dummy = inject.get(Dummy)) -> Dummy: + return x + + @injectable(wiring=None) + class NoWiring: + def __init__(self, x: Dummy = inject.me()) -> None: + self.dummy = x + + def method(self, x: Dummy = inject.me()) -> Dummy: + return x + + @inject + def injected_method(self, x: Dummy = inject.get(Dummy)) -> Dummy: + return x + + dummy = world.get(Dummy) + ww: WithWiring = world.get(WithWiring) + assert ww.dummy is dummy + assert ww.method() is dummy + assert ww.injected_method() is dummy + + nw: NoWiring = world.get(NoWiring) + assert nw.dummy is not dummy + assert nw.method() is not dummy + assert nw.injected_method() is dummy + + +def test_no_wiring() -> None: + @injectable + class Dummy: + pass + + @injectable(wiring=None) + class MyService: + def __init__(self, x: Dummy = inject.me()) -> None: + self.dummy = x + + def method(self, x: Inject[Dummy]) -> Dummy: + ... + + service: MyService = world.get(MyService) + assert isinstance(service, MyService) + assert not isinstance(service.dummy, Dummy) + + with pytest.raises(TypeError): + service.method() # type: ignore + + +def test_custom_wiring() -> None: + @injectable + class Dummy: + pass + + @injectable(wiring=Wiring(dependencies={'x': Dummy})) + class MyService: + def __init__(self, x: Dummy) -> None: + self.dummy = x + + def method(self, x: Dummy) -> Dummy: + return x + + service: MyService = world.get(MyService) + assert isinstance(service, MyService) + assert service.method() is world.get(Dummy) # type: ignore + + +@pytest.mark.parametrize('factory_method', ['static_method', 'class_method']) +def test_factory(factory_method: str) -> None: + sentinel = object() + + @injectable(factory_method=factory_method) + class Dummy: + def __init__(self, x: object): + self.x = x + + @classmethod + def class_method(cls) -> Dummy: + return Dummy(sentinel) + + @staticmethod + def static_method() -> Dummy: + return Dummy(sentinel) + + dummy: Dummy = world.get(Dummy) + assert isinstance(dummy, Dummy) + assert dummy is world.get(Dummy) + assert dummy.x is sentinel + + +@pytest.mark.parametrize('arg', + ['singleton', 'scope', 'wiring', 'factory_method', 'type_hints_locals']) +def test_invalid_arguments(arg: str) -> None: + with pytest.raises(TypeError, match=".*" + arg + ".*"): + @injectable(**{arg: object()}) # type: ignore + class Dummy: + ... + + +def test_invalid_class() -> None: + with pytest.raises(TypeError, match="(?i).*class.*"): + injectable(object()) # type: ignore + + with pytest.raises(TypeError, match="(?i).*class.*"): + injectable()(object()) # type: ignore + + +def test_invalid_factory_method() -> None: + with pytest.raises(AttributeError, match=".*build.*"): + @injectable(factory_method='build') + class Dummy: + ... + + with pytest.raises(TypeError, match=".*factory_method.*"): + @injectable(factory_method='build') + class Dummy2: + build = 1 + + with pytest.raises(TypeError, match=".*factory_method.*"): + @injectable(factory_method='build') + class Dummy3: + def build(self) -> None: + ... + + +def test_forbid_inheriting_service_class() -> None: + with pytest.raises(DuplicateDependencyError, match=".*Service.*"): + @injectable + class Dummy(Service): + ... + + +def test_duplicate_declaration() -> None: + with pytest.raises(DuplicateDependencyError, match=".*Dummy.*"): + @injectable + @injectable + class Dummy: + ... diff --git a/tests/lib/injectable/test_provider.py b/tests/lib/injectable/test_provider.py new file mode 100644 index 00000000..e4ae7ff9 --- /dev/null +++ b/tests/lib/injectable/test_provider.py @@ -0,0 +1,35 @@ +import pytest + +from antidote import injectable, world +from antidote.core.exceptions import DependencyNotFoundError +from antidote.lib.injectable import register_injectable_provider + + +def test_clone() -> None: + with world.test.empty(): + register_injectable_provider() + + @injectable + class Dummy: + pass + + class Service: + pass + + original_dummy = world.get(Dummy) + assert isinstance(original_dummy, Dummy) + + with world.test.clone(frozen=False): + new_dummy = world.get(Dummy) + assert isinstance(new_dummy, Dummy) + assert new_dummy is not original_dummy + + injectable(Service) + assert isinstance(world.get(Service), Service) + + assert world.get(Dummy) is original_dummy + with pytest.raises(DependencyNotFoundError): + world.get(Service) + + with world.test.clone(keep_singletons=True): + assert world.get(Dummy) is original_dummy diff --git a/tests/lib/interface/test_custom.py b/tests/lib/interface/test_custom.py index fa26d51e..605b250b 100644 --- a/tests/lib/interface/test_custom.py +++ b/tests/lib/interface/test_custom.py @@ -7,7 +7,7 @@ import pytest from antidote import implements, interface, world -from antidote._providers import ServiceProvider +from antidote.lib.injectable import register_injectable_provider from antidote.lib.interface import (NeutralWeight, Predicate, QualifiedBy, register_interface_provider) @@ -21,7 +21,7 @@ def _(x: T) -> T: @pytest.fixture(autouse=True) def setup_world() -> Iterator[None]: with world.test.empty(): - world.provider(ServiceProvider) + register_injectable_provider() register_interface_provider() yield @@ -268,15 +268,15 @@ def evaluate(self, predicate: Optional[LocaleIs]) -> bool: def test_lang_example() -> None: @interface class Alert: - ... # pragma: no cover + ... @_(implements(Alert).when(LocaleIs('fr'))) class FrenchAlert(Alert): - ... # pragma: no cover + ... @_(implements(Alert).when(LocaleIs('en'))) class DefaultAlert(Alert): - ... # pragma: no cover + ... assert world.get[Alert].single(LocaleIs("fr")) is world.get(FrenchAlert) assert world.get[Alert].single(LocaleIs("it")) is world.get(DefaultAlert) diff --git a/tests/lib/interface/test_interface.py b/tests/lib/interface/test_interface.py index b2e4e150..71877e8c 100644 --- a/tests/lib/interface/test_interface.py +++ b/tests/lib/interface/test_interface.py @@ -7,9 +7,9 @@ import pytest from typing_extensions import Protocol, runtime_checkable -from antidote import ImplementationsOf, implements, inject, interface, service, world -from antidote._providers import ServiceProvider +from antidote import ImplementationsOf, implements, inject, injectable, interface, world from antidote.core.exceptions import DependencyInstantiationError, DependencyNotFoundError +from antidote.lib.injectable import register_injectable_provider from antidote.lib.interface import QualifiedBy, register_interface_provider T = TypeVar('T') @@ -20,18 +20,6 @@ def _(x: T) -> T: return x -class Base: - pass - - -class GenericBase(Generic[Tco]): - pass - - -class GenericProtocolBase(Protocol[Tco]): - pass - - class Qualifier: def __init__(self, name: str) -> None: self.name = name @@ -53,13 +41,15 @@ class SubQualifier(Qualifier): @pytest.fixture(autouse=True) def setup_world() -> Iterator[None]: with world.test.empty(): - world.provider(ServiceProvider) + register_injectable_provider() register_interface_provider() yield def test_single_implementation() -> None: - interface(Base) + @interface + class Base: + pass @implements(Base) class Dummy(Base): @@ -122,7 +112,9 @@ def all_bases_iterable(x: Iterable[Base] = inject.me()) -> Iterable[Base]: def test_single_multiple_implementations_failure() -> None: - interface(Base) + @interface + class Base: + pass @implements(Base) class Dummy(Base): @@ -147,7 +139,9 @@ def f(x: Base = inject.me()) -> Base: def test_qualified_implementations() -> None: - interface(Base) + @interface + class Base: + pass @_(implements(Base).when(qualified_by=qA)) class A(Base): @@ -267,7 +261,9 @@ def test_invalid_interface() -> None: def test_invalid_implementation() -> None: - interface(Base) + @interface + class Base: + pass class BaseImpl(Base): pass @@ -286,7 +282,9 @@ class BaseImpl(Base): def test_unique_predicate() -> None: - interface(Base) + @interface + class Base: + pass class MyPred: def weight(self) -> Optional[Any]: @@ -304,10 +302,12 @@ class BaseImplV2(Base): def test_custom_service() -> None: - interface(Base) + @interface + class Base: + pass @implements(Base) - @service(singleton=False) + @injectable(singleton=False) class BaseImpl(Base): pass @@ -317,7 +317,9 @@ class BaseImpl(Base): def test_type_enforcement_if_possible() -> None: - interface(Base) + @interface + class Base: + pass with pytest.raises(TypeError, match="(?i).*subclass.*Base.*"): @implements(Base) @@ -345,8 +347,10 @@ class Invalid3: pass -def test_generic(): - interface(GenericBase) +def test_generic() -> None: + @interface + class GenericBase(Generic[Tco]): + pass @implements(GenericBase) class Dummy(GenericBase[int]): @@ -370,8 +374,10 @@ def f_all(x: List[GenericBase[int]] = inject.me()) -> List[GenericBase[int]]: assert f_all() == [dummy] -def test_generic_protocol(): - interface(GenericProtocolBase) +def test_generic_protocol() -> None: + @interface + class GenericProtocolBase(Protocol[Tco]): + pass @implements(GenericProtocolBase) class Dummy: diff --git a/tests/lib/interface/test_predicate.py b/tests/lib/interface/test_predicate.py index c9346df9..750732ce 100644 --- a/tests/lib/interface/test_predicate.py +++ b/tests/lib/interface/test_predicate.py @@ -3,14 +3,14 @@ import pytest from antidote import world -from antidote._providers import ServiceProvider +from antidote.lib.injectable import register_injectable_provider from antidote.lib.interface import NeutralWeight, register_interface_provider @pytest.fixture(autouse=True) def setup_world() -> Iterator[None]: with world.test.empty(): - world.provider(ServiceProvider) + register_injectable_provider() register_interface_provider() yield diff --git a/tests/lib/interface/test_provider.py b/tests/lib/interface/test_provider.py index 2e2d88d2..e0ed0ae2 100644 --- a/tests/lib/interface/test_provider.py +++ b/tests/lib/interface/test_provider.py @@ -1,16 +1,15 @@ import pytest from antidote import implements, interface, world -from antidote._providers import ServiceProvider from antidote.core.exceptions import DependencyNotFoundError +from antidote.lib.injectable import register_injectable_provider from antidote.lib.interface import register_interface_provider -from antidote.lib.interface._provider import Query def test_clone() -> None: with world.test.empty(): register_interface_provider() - world.provider(ServiceProvider) + register_injectable_provider() @interface class Base: @@ -20,16 +19,24 @@ class Base: class A(Base): pass + original_a = world.get(A) assert world.get[Base].single() is world.get(A) with world.test.clone(frozen=False): + new_a = world.get(A) + assert world.get[Base].single() is new_a + assert new_a is not original_a + @implements(Base) class B(Base): pass assert set(world.get[Base].all()) == {world.get(A), world.get(B)} - assert world.get[Base].single() is world.get(A) + with world.test.clone(keep_singletons=True): + assert world.get[Base].single() is original_a + + assert world.get[Base].single() is original_a def test_unknown_interface() -> None: @@ -39,4 +46,4 @@ class Dummy: with world.test.empty(): register_interface_provider() with pytest.raises(DependencyNotFoundError): - world.get[object](Query(interface=Dummy, constraints=[], all=False)) + world.get[object](Dummy) diff --git a/tests/lib/test_constants.py b/tests/lib/test_constants.py index 620ead64..9c8d7131 100644 --- a/tests/lib/test_constants.py +++ b/tests/lib/test_constants.py @@ -3,7 +3,8 @@ import pytest from antidote import const, Constants, inject, Wiring, world -from antidote._providers import LazyProvider, ServiceProvider +from antidote._providers import LazyProvider +from antidote.lib.injectable import register_injectable_provider from antidote.core.exceptions import DependencyInstantiationError from antidote.exceptions import DependencyNotFoundError @@ -16,7 +17,7 @@ class A: def test_world(): with world.test.empty(): world.provider(LazyProvider) - world.provider(ServiceProvider) + register_injectable_provider() yield diff --git a/tests/lib/test_factory.py b/tests/lib/test_factory.py index 58c86126..f149247b 100644 --- a/tests/lib/test_factory.py +++ b/tests/lib/test_factory.py @@ -4,7 +4,6 @@ import pytest from antidote import Factory, factory, inject, Inject, Provide, Service, Wiring, world -from antidote._providers import (FactoryProvider, LazyProvider, ServiceProvider) from antidote.exceptions import DependencyInstantiationError @@ -15,10 +14,7 @@ def does_not_raise(): @pytest.fixture(autouse=True) def test_world(): - with world.test.empty(): - world.provider(ServiceProvider) - world.provider(FactoryProvider) - world.provider(LazyProvider) + with world.test.new(): yield diff --git a/tests/lib/test_implementation.py b/tests/lib/test_implementation.py index 510cc5b6..d1bf9351 100644 --- a/tests/lib/test_implementation.py +++ b/tests/lib/test_implementation.py @@ -1,18 +1,13 @@ import pytest -from antidote import Factory, factory, implementation, inject, Provide, service, Service, world +from antidote import Factory, factory, implementation, inject, Provide, injectable, Service, world from antidote._implementation import validate_provided_class -from antidote._providers import (FactoryProvider, IndirectProvider, - ServiceProvider) from antidote.exceptions import DependencyInstantiationError @pytest.fixture(autouse=True) def test_world(): - with world.test.empty(): - world.provider(ServiceProvider) - world.provider(FactoryProvider) - world.provider(IndirectProvider) + with world.test.new(): yield @@ -365,7 +360,7 @@ def choose_a(s: B): def test_source_notation(): - @service + @injectable class A(Interface): pass diff --git a/tests/lib/test_lazy.py b/tests/lib/test_lazy.py index 0eb81218..5d4b822b 100644 --- a/tests/lib/test_lazy.py +++ b/tests/lib/test_lazy.py @@ -1,14 +1,11 @@ import pytest from antidote import LazyCall, LazyMethodCall, Service, world -from antidote._providers import LazyProvider, ServiceProvider @pytest.fixture(autouse=True) def empty_world(): - with world.test.empty(): - world.provider(LazyProvider) - world.provider(ServiceProvider) + with world.test.new(): yield diff --git a/tests/lib/test_service.py b/tests/lib/test_service.py index d25d9e39..49fc6caf 100644 --- a/tests/lib/test_service.py +++ b/tests/lib/test_service.py @@ -3,7 +3,6 @@ import pytest from antidote import Provide, Service, service, Wiring, world -from antidote._providers import ServiceProvider from antidote.exceptions import DuplicateDependencyError @@ -14,8 +13,7 @@ def does_not_raise(): @pytest.fixture(autouse=True) def test_world(): - with world.test.empty(): - world.provider(ServiceProvider) + with world.test.new(): yield diff --git a/tests/mypy_typing/test_readme.py b/tests/mypy_typing/test_readme.py deleted file mode 100644 index 8810b3e4..00000000 --- a/tests/mypy_typing/test_readme.py +++ /dev/null @@ -1,365 +0,0 @@ -# flake8: noqa -# Ignoring F811 for multiple definitions -from typing import Optional - -from typing_extensions import Annotated - - -def test_readme_simple() -> None: - from antidote import inject, service - - @service - class Database: - pass - - @inject - def f(db: Database = inject.me()) -> Database: - return db - - assert isinstance(f(), Database) # works ! - - ###################### - - f(Database()) - - ###################### - - from antidote import Inject - - @inject - def f2(db: Inject[Database]) -> None: - pass - - ###################### - - @inject([Database]) - def f3(db: object) -> None: - pass - - ###################### - - @inject({'db': Database}) - def f4(db: object) -> None: - pass - - ###################### - - from typing import Optional - - class Dummy: - pass - - # When the type_hint is optional and a marker like `inject.me()` is used, None will be - # provided if the dependency does not exists. - @inject - def f5(dummy: Optional[Dummy] = inject.me()) -> Optional[Dummy]: - return dummy - - assert f5() is None - - ###################### - - from antidote import world - - # Retrieve dependencies by hand, in tests typically - world.get(Database) - world.get[Database](Database) # with type hint - world.get[Database]() # omit dependency if it's the type hint itself - - ###################### - - from antidote import service, inject - - @service(singleton=False) - class QueryBuilder: - # methods are also injected by default - def __init__(self, db: Database = inject.me()) -> None: - self._db = db - - @inject - def load_data(builder: QueryBuilder = inject.me()) -> None: - pass - - load_data() # yeah ! - - ###################### - - from antidote import inject, Constants, const - - class Config(Constants): - DB_HOST = const[str]('localhost') - - @inject - def ping_db(db_host: str = Config.DB_HOST) -> None: - pass - - ping_db() # nice ! - - ###################### - - from antidote import inject, Constants, const - - class Config2(Constants): - DB_HOST = const[str]() # used as a type annotation - DB_PORT = const[int]() # and also to cast the value retrieved from `provide_const` - # defaults are supported, used on LookupError - DB_USER = const[str](default='postgres') - - def provide_const(self, *, name: str, arg: Optional[object]) -> object: - return os.environ[name] - - import os - os.environ['DB_HOST'] = 'localhost' - os.environ['DB_PORT'] = '5432' - - @inject - def check_connection(db_host: str = Config2.DB_HOST, - db_port: int = Config2.DB_PORT) -> None: - pass - - check_connection() # perfect ! - - ###################### - - from antidote import factory, inject - - class User: - pass - - @factory(singleton=False) # annotated type hints can be used or you can @inject manually - def current_user(db: Database = inject.me()) -> User: # return type annotation is used - return User() - - # Note that here you *know* exactly where it's coming from. - @inject - def is_admin(user: User = inject.me(source=current_user)) -> None: - pass - - ###################### - - from antidote import world - - world.get(User, source=current_user) - - ###################### - - from antidote import factory, inject, world - - REQUEST_SCOPE = world.scopes.new(name='request') - - @factory(scope=REQUEST_SCOPE) - def current_user2(db: Database = inject.me()) -> User: - return User() - - # Reset all dependencies in the specified scope. - world.scopes.reset(REQUEST_SCOPE) - - -def test_interface_impl() -> None: - from antidote import implementation, service, factory, Get - - class Cache: - pass - - @service - class MemoryCache(Cache): - pass - - class Redis: - """ class from an external library """ - - @factory - def redis_cache() -> Redis: - return Redis() - - @implementation(Cache) - def cache_impl() -> object: - import os - - if os.environ.get('USE_REDIS_CACHE'): - return Get(Redis, source=redis_cache) - - # Returning the dependency that must be retrieved - return MemoryCache - - ###################### - - from antidote import world, inject - - @inject - def heavy_compute(cache: Cache = inject.me(source=cache_impl)) -> None: - pass - - world.get(Cache, source=cache_impl) - - -def test_debugging() -> None: - from antidote import service, inject - - @service - class Database: - pass - - @inject - def f(db: Database = inject.me()) -> None: - pass - - f() - f(Database()) # test with specific arguments in unit tests - - ###################### - - from antidote import world - - # Clone current world to isolate it from the rest - with world.test.clone(): - x = object() - # Override the Database - world.test.override.singleton(Database, x) - f() # will have `x` injected for the Database - - @world.test.override.factory(Database) - def override_database() -> object: - class DatabaseMock: - pass - - return DatabaseMock() - - f() # will have `DatabaseMock()` injected for the Database - - -def test_readme() -> None: - # from a library - class ImdbAPI: - def __init__(self, host: str, port: int, api_key: str): - pass - - ###################### - - # movie.py - class MovieDB: - """ Interface """ - - def get_best_movies(self) -> None: - pass - - ###################### - - # config.py - from antidote import Constants, const - - class Config(Constants): - # with str/int/float, the type hint is enforced. Can be removed or extend to - # support Enums. - IMDB_HOST = const[str]('imdb.host') - IMDB_PORT = const[int]('imdb.port') - IMDB_API_KEY = const[str]('imdb.api_key') - - def __init__(self) -> None: - self._raw_conf = { - 'imdb': { - 'host': 'dummy_host', - 'api_key': 'dummy_api_key', - 'port': '80' - } - } - - def provide_const(self, *, name: str, arg: Optional[str]) -> object: - assert arg is not None - root, key = arg.split('.') - return self._raw_conf[root][key] - - ###################### - - # current_movie.py - # Code implementing/managing MovieDB - from antidote import factory, inject, Service, implementation - # from config import Config - - # Provides ImdbAPI, as defined by the return type annotation. - @factory - @inject([Config.IMDB_HOST, Config.IMDB_PORT, Config.IMDB_API_KEY]) - def imdb_factory(host: str, port: int, api_key: str) -> ImdbAPI: - # Here host = Config().provide_const('IMDB_HOST', 'imdb.host') - return ImdbAPI(host=host, port=port, api_key=api_key) - - class IMDBMovieDB(MovieDB, Service): - __antidote__ = Service.Conf(singleton=False) # New instance each time - - @inject({'imdb_api': ImdbAPI @ imdb_factory}) - def __init__(self, imdb_api: ImdbAPI) -> None: - self._imdb_api = imdb_api - - def get_best_movies(self) -> None: - pass - - @implementation(MovieDB) - def current_movie_db() -> object: - return IMDBMovieDB # dependency to be provided for MovieDB - - ###################### - - # current_movie.py - # Code implementing/managing MovieDB - from antidote import factory, Service, Get, From - # from typing import Annotated - # # from typing_extensions import Annotated # Python < 3.9 - # from config import Config - - @factory - def imdb_factory2(host: Annotated[str, Get(Config.IMDB_HOST)], - port: Annotated[int, Get(Config.IMDB_PORT)], - api_key: Annotated[str, Get(Config.IMDB_API_KEY)] - ) -> ImdbAPI: - return ImdbAPI(host, port, api_key) - - class IMDBMovieDB2(MovieDB, Service): - __antidote__ = Service.Conf(singleton=False) - - def __init__(self, imdb_api: Annotated[ImdbAPI, From(imdb_factory2)]): - self._imdb_api = imdb_api - - def get_best_movies(self) -> None: - pass - - ###################### - - # main.py - # from movie import MovieDB - # from current_movie import current_movie_db - - @inject([MovieDB @ current_movie_db]) - def main(movie_db: Optional[MovieDB] = None) -> None: - assert movie_db is not None # for Mypy, to understand that movie_db is optional - pass - - # Or with annotated type hints - @inject - def main2(movie_db: Annotated[MovieDB, From(current_movie_db)]) -> None: - pass - - main2() - - ###################### - - conf = Config() - main2(IMDBMovieDB(imdb_factory( - # constants can be retrieved directly on an instance - host=conf.IMDB_HOST, - port=conf.IMDB_PORT, - api_key=conf.IMDB_API_KEY, - ))) - - ###################### - - from antidote import world - - # Clone current world to isolate it from the rest - with world.test.clone(): - # Override the configuration - world.test.override.singleton(Config.IMDB_HOST, 'other host') - main2() - - ###################### - - world.debug(main2) diff --git a/tests/mypy_typing/test_type_cast.py b/tests/mypy_typing/test_type_cast.py index df47409e..50d9645f 100644 --- a/tests/mypy_typing/test_type_cast.py +++ b/tests/mypy_typing/test_type_cast.py @@ -57,7 +57,7 @@ def g(dummy: Optional[Annotated[Dummy, From(build_dummy)]] = None) -> Dummy: assert dummy is not None return dummy - assert g().hello() is world.get[Dummy](Dummy @ build_dummy) + assert g().hello() is world.get[Dummy](Dummy @ build_dummy) # type: ignore def test_proper_typing_assert_none() -> None: diff --git a/tests/providers/test_factory.py b/tests/providers/test_factory.py index 5dd08b24..516a4009 100644 --- a/tests/providers/test_factory.py +++ b/tests/providers/test_factory.py @@ -2,7 +2,7 @@ from antidote import Scope, world from antidote._providers import FactoryProvider -from antidote._providers.service import Parameterized +from antidote.lib.injectable._provider import Parameterized from antidote.exceptions import (DependencyNotFoundError, DuplicateDependencyError, FrozenWorldError) diff --git a/tests/providers/test_indirect.py b/tests/providers/test_indirect.py index 03908fa1..2c53f478 100644 --- a/tests/providers/test_indirect.py +++ b/tests/providers/test_indirect.py @@ -3,7 +3,8 @@ import pytest from antidote import Scope, world -from antidote._providers import IndirectProvider, ServiceProvider +from antidote._providers import IndirectProvider +from antidote.lib.injectable._provider import InjectableProvider from antidote.core.exceptions import DependencyNotFoundError from antidote.exceptions import DuplicateDependencyError, FrozenWorldError @@ -28,8 +29,8 @@ def empty_world(): @pytest.fixture def service(empty_world): - world.provider(ServiceProvider) - return world.get(ServiceProvider) + world.provider(InjectableProvider) + return world.get(InjectableProvider) @pytest.fixture @@ -55,7 +56,7 @@ def test_implementation(permanent: bool): @pytest.mark.parametrize('singleton', [True, False]) @pytest.mark.parametrize('permanent', [True, False]) -def test_implementation_permanent_singleton(service: ServiceProvider, +def test_implementation_permanent_singleton(service: InjectableProvider, singleton: bool, permanent: bool): scope = Scope.singleton() if singleton else None diff --git a/tests/providers/test_providers.py b/tests/providers/test_providers.py index c688a104..95ec03ee 100644 --- a/tests/providers/test_providers.py +++ b/tests/providers/test_providers.py @@ -1,14 +1,12 @@ import pytest from antidote import world -from antidote._providers import (FactoryProvider, IndirectProvider, - LazyProvider, ServiceProvider) +from antidote._providers import FactoryProvider, IndirectProvider, LazyProvider from antidote.core.container import RawProvider @pytest.fixture(params=[ FactoryProvider, - ServiceProvider, LazyProvider, IndirectProvider ]) diff --git a/tests/providers/test_service.py b/tests/providers/test_service.py index 6eeb0e25..25b2fbad 100644 --- a/tests/providers/test_service.py +++ b/tests/providers/test_service.py @@ -1,7 +1,7 @@ import pytest from antidote import Scope, world -from antidote._providers.service import Parameterized, ServiceProvider +from antidote.lib.injectable._provider import Parameterized, InjectableProvider from antidote.exceptions import (DependencyNotFoundError, DuplicateDependencyError, FrozenWorldError) @@ -9,8 +9,8 @@ @pytest.fixture def provider(): with world.test.empty(): - world.provider(ServiceProvider) - yield world.get(ServiceProvider) + world.provider(InjectableProvider) + yield world.get(InjectableProvider) @pytest.fixture(params=[ @@ -69,7 +69,7 @@ def test_parameterized_eq_hash(): assert a != x -def test_simple(provider: ServiceProvider, scope: Scope): +def test_simple(provider: InjectableProvider, scope: Scope): provider.register(A, scope=scope) assert isinstance(world.get(A), A) assert repr(A) in repr(provider) @@ -78,14 +78,14 @@ def test_simple(provider: ServiceProvider, scope: Scope): @pytest.mark.parametrize('singleton', [True, False]) def test_register(singleton: bool): with world.test.empty(): - provider = ServiceProvider() + provider = InjectableProvider() provider.register(A, scope=Scope.singleton() if singleton else None) assert world.test.maybe_provide_from(provider, A).is_singleton() is singleton assert isinstance(world.test.maybe_provide_from(provider, A).unwrapped, A) def test_parameterized(scope: Scope): - provider = ServiceProvider() + provider = InjectableProvider() provider.register(A, scope=scope) s = world.test.maybe_provide_from(provider, @@ -94,7 +94,7 @@ def test_parameterized(scope: Scope): assert dict(val=object) == s.kwargs -def test_duplicate_error(provider: ServiceProvider, scope: Scope): +def test_duplicate_error(provider: InjectableProvider, scope: Scope): provider.register(A, scope=scope) with pytest.raises(DuplicateDependencyError): @@ -102,7 +102,7 @@ def test_duplicate_error(provider: ServiceProvider, scope: Scope): @pytest.mark.parametrize('keep_singletons_cache', [True, False]) -def test_copy(provider: ServiceProvider, +def test_copy(provider: InjectableProvider, keep_singletons_cache: bool, scope: Scope): class C: pass @@ -136,14 +136,14 @@ class E: world.get(E) -def test_freeze(provider: ServiceProvider, scope: Scope): +def test_freeze(provider: InjectableProvider, scope: Scope): world.freeze() with pytest.raises(FrozenWorldError): provider.register(A, scope=scope) -def test_exists(provider: ServiceProvider, scope: Scope): +def test_exists(provider: InjectableProvider, scope: Scope): provider.register(A, scope=scope) assert not provider.exists(object()) assert provider.exists(A) @@ -151,7 +151,7 @@ def test_exists(provider: ServiceProvider, scope: Scope): assert not provider.exists(Parameterized(B, dict(a=1))) -def test_custom_scope(provider: ServiceProvider): +def test_custom_scope(provider: InjectableProvider): dummy_scope = world.scopes.new(name='dummy') class MyService: @@ -169,6 +169,6 @@ class MyService: pytest.param(object(), None, id='klass'), pytest.param(A, object(), id='scope') ]) -def test_sanity_checks(provider: ServiceProvider, klass, scope): +def test_sanity_checks(provider: InjectableProvider, klass, scope): with pytest.raises((AssertionError, TypeError)): provider.register(klass, scope=scope) diff --git a/tests/test_class_auto_wiring.py b/tests/test_class_auto_wiring.py index ab5ffd17..11865de7 100644 --- a/tests/test_class_auto_wiring.py +++ b/tests/test_class_auto_wiring.py @@ -4,7 +4,7 @@ import pytest from typing_extensions import Protocol -from antidote import Constants, Factory, Provide, Service, service, world +from antidote import Constants, Factory, injectable, Provide, Service, service, world from antidote.core import Wiring @@ -127,6 +127,7 @@ def __call__(self) -> FactoryOutput2: # for Factory @pytest.fixture(params=[ pytest.param((builder, service), id="@service"), + pytest.param((builder, injectable), id="@injectable"), *[ pytest.param((builder, c, w), id=f"{c.__name__} - {w}") for (c, w) in itertools.product([Factory, diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 00000000..f9e28b6e --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,8 @@ +import pytest + +from antidote import config + + +def test_invalid_auto_detect_type_hints_locals(): + with pytest.raises(TypeError, match=".*auto_detect_type_hints_locals.*"): + config.auto_detect_type_hints_locals = 'auto' diff --git a/tests/world/test_test.py b/tests/world/test_test.py index a2dc0ba8..3c9a9c40 100644 --- a/tests/world/test_test.py +++ b/tests/world/test_test.py @@ -3,7 +3,7 @@ import pytest from antidote import world -from antidote._providers import ServiceProvider +from antidote.lib.injectable._provider import InjectableProvider from antidote.core import (Container, DependencyValue, StatelessProvider) from antidote.core.exceptions import FrozenWorldError from antidote.exceptions import DependencyNotFoundError @@ -67,7 +67,7 @@ def provide(self, dependency: float, container: Container with pytest.raises(DependencyNotFoundError): world.get(DummyIntProvider) with pytest.raises(DependencyNotFoundError): - world.get(ServiceProvider) + world.get(InjectableProvider) if strategy != 'clone': world.test.singleton("y", y) @@ -138,7 +138,7 @@ def test_empty(): world.provider(DummyIntProvider) assert world.get(10) == 20 with pytest.raises(DependencyNotFoundError): - world.get(ServiceProvider) + world.get(InjectableProvider) with pytest.raises(DependencyNotFoundError): world.get("a") diff --git a/tests/world/test_world.py b/tests/world/test_world.py index 35999181..329f81ff 100644 --- a/tests/world/test_world.py +++ b/tests/world/test_world.py @@ -5,7 +5,8 @@ from antidote import factory, From, FromArg, Get, Service, world from antidote._internal.world import LazyDependency -from antidote._providers import FactoryProvider, ServiceProvider +from antidote.lib.injectable._provider import InjectableProvider +from antidote._providers import FactoryProvider from antidote.exceptions import DependencyNotFoundError, FrozenWorldError from .utils import DummyIntProvider @@ -21,7 +22,7 @@ class A: def test_get(): - world.provider(ServiceProvider) + world.provider(InjectableProvider) a = A() world.test.singleton(A, a) assert world.get(A) is a @@ -141,8 +142,8 @@ def build_a() -> A: def test_freeze(): - world.provider(ServiceProvider) - provider = world.get[ServiceProvider]() + world.provider(InjectableProvider) + provider = world.get[InjectableProvider]() class Service: pass diff --git a/tox.ini b/tox.ini index fedf76af..12497516 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ distshare = {toxworkdir}/distshare python = 3.7: py37{,-compiled} 3.8: py38{,-compiled} - 3.9: py39{,-compiled},flake8,manifest,docs,pyright,rstvalidator + 3.9: py39{,-compiled},flake8,manifest,docs,pyright,mypy,rstvalidator 3.10: py310{,-compiled} [testenv] @@ -34,22 +34,21 @@ commands = [testenv:mypy] -ignore_outcome = true changedir = {toxinidir} deps = - mypy==0.941 + mypy==0.942 mypy-extensions==0.4.3 commands = - mypy --strict src tests/mypy_typing + ; some cast / ignores are for PyRight. + mypy --no-warn-redundant-casts --no-warn-unused-ignores --cache-dir=/dev/null [testenv:pyright] changedir = {toxinidir} deps = - pyright==1.1.230 + pyright==1.1.239 commands = - pyright src - pyright tests/mypy_typing + pyright [testenv:flake8] @@ -93,7 +92,7 @@ commands = [testenv:coverage-report] parallel_show_output = true -depends = pypy3,py{36,37,38,39}{,-compiled} +depends = py{37,38,39,310}{,-compiled} changedir = {toxinidir} skip_install = true setenv = @@ -101,7 +100,7 @@ setenv = deps = coverage[toml]==6.3.2 commands = coverage combine - coverage report --skip-covered + coverage report --skip-covered --show-missing coverage html coverage xml