diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e6e661e..04e06d2 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -32,22 +32,23 @@ jobs: # run: | # tox -e lint-git -# lint: + lint: # name: "Linting and type checking" -# runs-on: ubuntu-24.04 + name: "Linting" + runs-on: ubuntu-24.04 # strategy: # fail-fast: true -# steps: -# - uses: actions/checkout@v4 -# - uses: actions/setup-python@v5 -# with: -# python-version: "3.13" -# - name: "Install tox" -# run: | -# pip install tox -# - name: "Lint formatting" -# run: | -# tox -e lint-py + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: "Install tox" + run: | + pip install tox + - name: "Lint formatting" + run: | + tox -e lint-py # - name: "Type checking" # run: | # tox -e lint-mypy diff --git a/pyproject.toml b/pyproject.toml index 2433b81..bc872a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,43 +29,26 @@ homepage = "https://github.com/ivankorobkov/python-inject" source = "https://github.com/ivankorobkov/python-inject" issues = "https://github.com/ivankorobkov/python-inject/issues" + [dependency-groups] +dev = [ + "ipython", + "ruff", + "mypy", + "yamllint", + { include-group = "tests" }, +] + tests = [ "pytest", "pytest-cov", ] -dev = [ - "ipython", - { include-group = "tests" }, -] [build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" -[tool.black] -line-length = 120 -skip-string-normalization = true -target_version = ["py39", "py310", "py311"] -include = '\.pyi?$' -exclude = ''' -/( - \.eggs - | \.git - | \.github - | \.hg - | \.idea - | \.mypy_cache - | \.tox - | \.pyre_configuration - | \.venv - | _build - | build - | dist - | var -) -''' [tool.hatch.build.hooks.vcs] version-file = "src/inject/_version.py" @@ -78,6 +61,7 @@ packages = ["src/inject"] [tool.hatch.version] source = "vcs" + [tool.isort] case_sensitive = true include_trailing_comma = true @@ -85,6 +69,7 @@ line_length = 120 multi_line_output = 3 profile = "black" + [tool.mypy] check_untyped_defs = true disallow_any_generics = true @@ -103,6 +88,8 @@ warn_unused_ignores = true #strict = true # TODO(pyctrl): improve typings and enable strict mode exclude = ["tests", ".venv"] + +# TODO(pyctrl): deprecate pyright — stay with mypy [tool.pyright] defineConstant = { DEBUG = true } exclude = [] @@ -114,69 +101,133 @@ pythonVersion = "3.11" reportMissingImports = true reportMissingTypeStubs = false + [tool.ruff] -ignore = [ - "B027", # Allow non-abstract empty methods in abstract base classes - "FBT003", # Allow boolean positional values in function calls, like `dict.get(... True)` - # Ignore checks for possible passwords - "S105", - "S106", - "S107", - # Ignore complexity - "C901", - "PLR0911", - "PLR0912", - "PLR0913", - "PLR0915", - "PLC1901", # empty string comparisons - "PLW2901", # `for` loop variable overwritten - "SIM114", # Combine `if` branches using logical `or` operator +line-length = 88 +extend-exclude = [".git", ".venv", "docs"] + +[tool.ruff.lint] +preview = true +extend-select = ["ALL"] +extend-ignore = [ + "D10", # missing documentation + "D203", # 1 of conflicting code-styles + "D212", # 1 of conflicting code-styles + "C408", # allow `dict()` instead of literal + "TD003", # don't require issue link + # Completely disable + "FIX", + "CPY", + # formatter conflict rules + "W191", + "E111", + "E114", + "E117", + "EM101", + "EM102", + "ERA001", # commented code + "D206", + "D300", + "DOC201", + "DOC402", + "Q000", + "Q001", + "Q002", + "Q003", + "COM812", + "COM819", + "ISC001", + "ISC002", + "N818", # Exception name should be named with an Error suffix + "TRY003", + "UP006", + "UP007", + "UP035", + "UP045", + "FA100", ] -line-length = 120 -select = [ - "A", - "B", - "C", - "DTZ", - "E", - "EM", - "F", - "FBT", - "I", - "ICN", - "ISC", - "N", - "PLC", - "PLE", - "PLR", - "PLW", - "Q", - "RUF", - "S", - "SIM", - "T", - "TID", - "UP", - "W", - "YTT", -] -target-version = ["py39", "py310", "py311"] -unfixable = [ - "F401", # Don't touch unused imports + +[tool.ruff.lint.extend-per-file-ignores] +"**/tests/**/test_*.py" = [ + "ANN", # annotations not required in tests + "E731", # Do not assign a `lambda` expression, use a `def` + "PLC2701", # Private name import + "PLR0913", # Too many arguments in function definition + "PLR0917", # Too many positional arguments + "PLR2004", # allow "magic" values in tests + "PLR6301", # Method could be a function, class method, or static method + "PT017", # Found assertion on exception `except` block, use `pytest.raises()` instead + "PT027", # Use `pytest.raises` instead of unittest-style `assertRaisesRegex` + "S101", # allow asserts + "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + "SLF001", # allow private member access ] +"/**/tests/__init__.py" = ["PLR6301"] -[tool.ruff.flake8-quotes] -inline-quotes = "single" +[tool.ruff.lint.flake8-import-conventions.extend-aliases] +"typing" = "t" -[tool.ruff.flake8-tidy-imports] -ban-relative-imports = "all" +[tool.ruff.lint.flake8-import-conventions] +banned-from = ["dataclasses", "inject", "typing"] -[tool.ruff.isort] +[tool.ruff.lint.isort] +force-single-line = true known-first-party = ["inject"] -[tool.ruff.per-file-ignores] -# Tests can use magic values, assertions, and relative imports -"/**/test_*.py" = ["PLR2004", "S101", "TID252"] +[tool.ruff.lint.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" + +#ignore = [ +# "B027", # Allow non-abstract empty methods in abstract base classes +# "FBT003", # Allow boolean positional values in function calls, like `dict.get(... True)` +# # Ignore checks for possible passwords +# "S105", +# "S106", +# "S107", +# # Ignore complexity +# "C901", +# "PLR0911", +# "PLR0912", +# "PLR0913", +# "PLR0915", +# "PLC1901", # empty string comparisons +# "PLW2901", # `for` loop variable overwritten +# "SIM114", # Combine `if` branches using logical `or` operator +#] +#select = [ +# "A", +# "B", +# "C", +# "DTZ", +# "E", +# "EM", +# "F", +# "FBT", +# "I", +# "ICN", +# "ISC", +# "N", +# "PLC", +# "PLE", +# "PLR", +# "PLW", +# "Q", +# "RUF", +# "S", +# "SIM", +# "T", +# "TID", +# "UP", +# "W", +# "YTT", +#] +#unfixable = [ +# "F401", # Don't touch unused imports +#] #### Pytest & Coverage #### @@ -219,20 +270,23 @@ env_list = [ "py311", "py312", "py313", + "fmt-py", "fmt-toml", + "lint-py", #"lint-mypy", # TODO(pyctrl): make it green & uncomment "lint-toml", "lint-yaml", + #"lint-git", "coverage", ] [tool.tox.labels] fmt = [ - # "fmt-py", + "fmt-py", "fmt-toml", ] lint = [ - #"lint-py", + "lint-py", #"lint-mypy", # TODO(pyctrl): make it green & uncomment "lint-toml", "lint-yaml", @@ -247,6 +301,28 @@ dependency_groups = ["tests"] commands = [["pytest", { replace = "posargs", extend = true }]] # tox envs +[tool.tox.env.lint-py] +description = "Lint python files" +deps = ["ruff"] +skip_install = true +commands = [ + [ + "ruff", + "format", + "--diff", + { replace = "posargs", default = [ + "{tox_root}", + ], extend = true }, + ], + [ + "ruff", + "check", + { replace = "posargs", default = [ + "{tox_root}", + ], extend = true }, + ], +] + [tool.tox.env.lint-mypy] description = "Type checking" deps = ["mypy"] @@ -275,6 +351,25 @@ commands = [ ], ] +[tool.tox.env.fmt-py] +description = "Format python files" +deps = ["ruff"] +skip_install = true +commands = [ + [ + "ruff", + "format", + { replace = "posargs", default = ["{tox_root}"], extend = true }, + ], + [ + "ruff", + "check", + "--fix", + "--show-fixes", + { replace = "posargs", default = ["{tox_root}"], extend = true }, + ], +] + [tool.tox.env.fmt-toml] description = "Format TOML files" allowlist_externals = ["taplo"] diff --git a/src/inject/__init__.py b/src/inject/__init__.py index 8517a16..851f310 100644 --- a/src/inject/__init__.py +++ b/src/inject/__init__.py @@ -72,163 +72,186 @@ def my_config(binder): inject.configure(my_config) -""" +""" # noqa: E501 + from __future__ import annotations import contextlib - -from inject._version import __version__ - +import functools import inspect import logging import sys import threading -from functools import wraps -from typing import (Any, Awaitable, Callable, Dict, Generic, Hashable, - Optional, Set, Type, TypeVar, Union, cast, get_type_hints, - overload, no_type_check) +import typing as t -_HAS_PEP604_SUPPORT = sys.version_info[:3] >= (3, 10, 0) # PEP 604 -if _HAS_PEP604_SUPPORT: - _HAS_PEP560_SUPPORT = True -else: - _HAS_PEP560_SUPPORT = sys.version_info[:3] >= (3, 7, 0) # PEP 560 -_RETURN = 'return' +from inject._version import __version__ as __version__ + +# PEP 604 +_HAS_PEP604_SUPPORT = sys.version_info[:3] >= (3, 10, 0) +# PEP 560 +_HAS_PEP560_SUPPORT = _HAS_PEP604_SUPPORT or sys.version_info[:3] >= (3, 7, 0) + +_RETURN = "return" _MISSING = object() if _HAS_PEP604_SUPPORT: from types import UnionType - from typing import ForwardRef, _GenericAlias + from typing import _GenericAlias # noqa: ICN003 elif _HAS_PEP560_SUPPORT: - from typing import ForwardRef, _GenericAlias + from typing import _GenericAlias # noqa: ICN003, PLC2701 else: - from typing import _Union + from typing import _Union # noqa: ICN003, PLC2701 -logger = logging.getLogger('inject') +logger = logging.getLogger("inject") _INJECTOR = None # Shared injector instance. _INJECTOR_LOCK = threading.RLock() # Guards injector initialization. _BINDING_LOCK = threading.RLock() # Guards runtime bindings. -Injectable = Union[object, Any] -T = TypeVar('T', bound=Injectable) -Binding = Union[Type[Injectable], Hashable] -Constructor = Callable[[], Injectable] +Injectable = t.Union[object, t.Any] +T = t.TypeVar("T", bound=Injectable) +Binding = t.Union[type[Injectable], t.Hashable] +Constructor = t.Callable[[], Injectable] Provider = Constructor -BinderCallable = Callable[['Binder'], Optional['Binder']] +BinderCallable = t.Callable[["Binder"], t.Optional["Binder"]] class ConstructorTypeError(TypeError): - def __init__(self, constructor: Callable, previous_error: TypeError): - super(ConstructorTypeError, self).__init__("%s raised an error: %s" % (constructor, previous_error)) + def __init__(self, constructor: t.Callable, previous_error: TypeError) -> None: + super().__init__(f"{constructor} raised an error: {previous_error}") -class Binder(object): - _bindings: Dict[Binding, Constructor] +class Binder: + _bindings: dict[Binding, Constructor] - def __init__(self, allow_override: bool = False) -> None: + def __init__(self, allow_override: bool = False) -> None: # noqa: FBT001, FBT002 self._bindings = {} self.allow_override = allow_override - def install(self, config: BinderCallable) -> 'Binder': + def install(self, config: BinderCallable) -> Binder: """Install another callable configuration.""" config(self) return self - def bind(self, cls: Binding, instance: T) -> 'Binder': + def bind(self, cls: Binding, instance: T) -> Binder: """Bind a class to an instance.""" self._check_class(cls) - b = lambda: instance + b = lambda: instance # noqa: E731 self._bindings[cls] = b self._maybe_bind_forward(cls, b) - logger.debug('Bound %s to an instance %s', cls, instance) + logger.debug("Bound %s to an instance %s", cls, instance) return self - def bind_to_constructor(self, cls: Binding, constructor: Constructor) -> 'Binder': - """Bind a class to a callable singleton constructor.""" + def bind_to_constructor(self, cls: Binding, constructor: Constructor) -> Binder: + """ + Bind a class to a callable singleton constructor. + + Raises: + InjectorException: if no constructor + + """ self._check_class(cls) if constructor is None: - raise InjectorException('Constructor cannot be None, key=%s' % cls) - + raise InjectorException(f"Constructor cannot be None, key={cls}") + b = _ConstructorBinding(constructor) self._bindings[cls] = b self._maybe_bind_forward(cls, b) - logger.debug('Bound %s to a constructor %s', cls, constructor) + logger.debug("Bound %s to a constructor %s", cls, constructor) return self - def bind_to_provider(self, cls: Binding, provider: Provider) -> 'Binder': + def bind_to_provider(self, cls: Binding, provider: Provider) -> Binder: """ Bind a class to a callable instance provider executed for each injection. - A provider can be a normal function or a context manager. Both sync and async are supported. + + A provider can be a normal function or a context manager. + Both sync and async are supported. + + Raises: + InjectorException: if no provider + """ self._check_class(cls) if provider is None: - raise InjectorException('Provider cannot be None, key=%s' % cls) + raise InjectorException(f"Provider cannot be None, key={cls}") b = provider self._bindings[cls] = b self._maybe_bind_forward(cls, b) - logger.debug('Bound %s to a provider %s', cls, provider) + logger.debug("Bound %s to a provider %s", cls, provider) return self def _check_class(self, cls: Binding) -> None: if cls is None: - raise InjectorException('Binding key cannot be None') + raise InjectorException("Binding key cannot be None") if not self.allow_override and cls in self._bindings: - raise InjectorException('Duplicate binding, key=%s' % cls) + raise InjectorException(f"Duplicate binding, key={cls}") if self._is_forward_str(cls): - ref = ForwardRef(cls) + ref = t.ForwardRef(cls) if not self.allow_override and ref in self._bindings: - raise InjectorException('Duplicate forward binding, i.e. "int" and int, key=%s', cls) - - def _maybe_bind_forward(self, cls: Binding, binding: Any) -> None: + msg = f'Duplicate forward binding, i.e. "int" and int, key={cls}' + raise InjectorException(msg) + + def _maybe_bind_forward(self, cls: Binding, binding: t.Any) -> None: # noqa: ANN401 """Bind a string forward reference.""" if not _HAS_PEP560_SUPPORT: return if not isinstance(cls, str): return - - ref = ForwardRef(cls) + + ref = t.ForwardRef(cls) self._bindings[ref] = binding logger.debug('Bound forward ref "%s"', cls) - def _is_forward_str(self, cls: Binding) -> bool: - return _HAS_PEP560_SUPPORT and isinstance(cls, str) + @staticmethod + def _is_forward_str(kls: Binding) -> bool: + return _HAS_PEP560_SUPPORT and isinstance(kls, str) -class Injector(object): - _bindings: Dict[Binding, Constructor] +class Injector: + _bindings: dict[Binding, Constructor] def __init__( - self, config: Optional[BinderCallable] = None, bind_in_runtime: bool = True, allow_override: bool = False - ): + self, + config: t.Optional[BinderCallable] = None, + # TODO(pyctrl): force following flags to be kwargs + bind_in_runtime: bool = True, # noqa: FBT001, FBT002 + allow_override: bool = False, # noqa: FBT001, FBT002 + ) -> None: self._bind_in_runtime = bind_in_runtime if config: binder = Binder(allow_override) config(binder) - self._bindings = binder._bindings + self._bindings = binder._bindings # noqa: SLF001 else: self._bindings = {} # NOTE(pyctrl): only since 3.12 - # @overload - # def get_instance(self, cls: Type[T]) -> T: ... + # @t.overload + # def get_instance(self, cls: type[T]) -> T: ... - @overload + @t.overload def get_instance(self, cls: Binding) -> T: ... - @overload - def get_instance(self, cls: Hashable) -> Injectable: ... + @t.overload + def get_instance(self, cls: t.Hashable) -> Injectable: ... def get_instance(self, cls: Binding) -> Injectable: - """Return an instance for a class.""" + """ + Return an instance for a class. + + Raises: + InjectorException: on errors + ConstructorTypeError: over TypeError + + """ binding = self._bindings.get(cls) if binding: return binding() @@ -240,22 +263,25 @@ def get_instance(self, cls: Binding) -> Injectable: return binding() if not self._bind_in_runtime: - raise InjectorException( - 'No binding was found for key=%s' % cls) + msg = f"No binding was found for key={cls}" + raise InjectorException(msg) if not callable(cls): - raise InjectorException( - 'Cannot create a runtime binding, the key is not callable, key=%s' % cls) + msg = ( + "Cannot create a runtime binding, the key is not callable," + f" key={cls}", + ) + raise InjectorException(msg) try: instance = cls() except TypeError as previous_error: - raise ConstructorTypeError(cls, previous_error) + raise ConstructorTypeError(cls, previous_error) # noqa: B904 self._bindings[cls] = lambda: instance - logger.debug( - 'Created a runtime binding for key=%s, instance=%s', cls, instance) + msg = "Created a runtime binding for key=%s, instance=%s" + logger.debug(msg, cls, instance) return instance @@ -263,10 +289,10 @@ class InjectorException(Exception): pass -class _ConstructorBinding(Generic[T]): - _instance: Optional[T] +class _ConstructorBinding(t.Generic[T]): + _instance: t.Optional[T] - def __init__(self, constructor: Callable[[], T]) -> None: + def __init__(self, constructor: t.Callable[[], T]) -> None: self._constructor = constructor self._created = False self._instance = None @@ -319,136 +345,162 @@ def __call__(self) -> T: # - `attr` implementation is inherited from `property` # - `attr` class member is not annotated class _AttributeInjection(property): - def __init__(self, cls: Type[T] | Hashable) -> None: + def __init__(self, cls: t.Union[type[T], t.Hashable]) -> None: self._cls = cls super().__init__( fget=lambda _: instance(self._cls), doc="Return an attribute injection", ) - def __set_name__(self, owner: Type[T], name: str) -> None: + def __set_name__(self, owner: type[T], name: str) -> None: if self._cls is _MISSING: self._cls = _unwrap_cls_annotation(owner, name) -class _ParameterInjection(Generic[T]): - __slots__ = ('_name', '_cls') +class _ParameterInjection(t.Generic[T]): + __slots__ = ("_cls", "_name") - def __init__(self, name: str, cls: Optional[Binding] = None) -> None: + def __init__(self, name: str, cls: t.Optional[Binding] = None) -> None: self._name = name self._cls = cls - def __call__(self, func: Callable[..., Union[T, Awaitable[T]]]) -> Callable[..., Union[T, Awaitable[T]]]: + def __call__( + self, func: t.Callable[..., t.Union[T, t.Awaitable[T]]] + ) -> t.Callable[..., t.Union[T, t.Awaitable[T]]]: if inspect.iscoroutinefunction(func): - @wraps(func) - async def async_injection_wrapper(*args: Any, **kwargs: Any) -> T: + + @functools.wraps(func) + async def async_injection_wrapper(*args: t.Any, **kwargs: t.Any) -> T: # noqa: ANN401 if self._name not in kwargs: kwargs[self._name] = instance(self._cls or self._name) - async_func = cast(Callable[..., Awaitable[T]], func) + async_func = t.cast("t.Callable[..., t.Awaitable[T]]", func) return await async_func(*args, **kwargs) + return async_injection_wrapper - - @wraps(func) - def injection_wrapper(*args: Any, **kwargs: Any) -> T: + + @functools.wraps(func) + def injection_wrapper(*args: t.Any, **kwargs: t.Any) -> T: # noqa: ANN401 if self._name not in kwargs: kwargs[self._name] = instance(self._cls or self._name) - sync_func = cast(Callable[..., T], func) + sync_func = t.cast("t.Callable[..., T]", func) return sync_func(*args, **kwargs) return injection_wrapper -class _ParametersInjection(Generic[T]): - __slots__ = ('_params', ) +class _ParametersInjection(t.Generic[T]): + __slots__ = ("_params",) - def __init__(self, **kwargs: Any) -> None: + def __init__(self, **kwargs: Binding) -> None: self._params = kwargs @staticmethod def _aggregate_sync_stack( - sync_stack: contextlib.ExitStack, - provided_params: frozenset[str], - kwargs: dict[str, Any] + sync_stack: contextlib.ExitStack, + provided_params: frozenset[str], + kwargs: dict[str, t.Any], ) -> None: - """Extracts context managers, aggregate them in an ExitStack and swap out the param value with results of - running __enter__(). The result is equivalent to using `with` multiple times """ + """ + Manage context stack. + + Extracts context managers, aggregate them in an ExitStack + and swap out the param value with results of running `__enter__()`. + The result is equivalent to using `with` multiple times. + """ executed_kwargs = { param: sync_stack.enter_context(inst) for param, inst in kwargs.items() - if param not in provided_params and isinstance(inst, contextlib._GeneratorContextManager) + if param not in provided_params + and isinstance(inst, contextlib._GeneratorContextManager) # noqa: SLF001 } kwargs.update(executed_kwargs) @staticmethod async def _aggregate_async_stack( - async_stack: contextlib.AsyncExitStack, - provided_params: frozenset[str], - kwargs: dict[str, Any] + async_stack: contextlib.AsyncExitStack, + provided_params: frozenset[str], + kwargs: dict[str, t.Any], ) -> None: - """Similar to _aggregate_sync_stack, but for async context managers""" + """Similar to _aggregate_sync_stack, but for async context managers.""" executed_kwargs = { param: await async_stack.enter_async_context(inst) for param, inst in kwargs.items() - if param not in provided_params and isinstance(inst, contextlib._AsyncGeneratorContextManager) + if param not in provided_params + and isinstance(inst, contextlib._AsyncGeneratorContextManager) # noqa: SLF001 } kwargs.update(executed_kwargs) - def __call__(self, func: Callable[..., Union[Awaitable[T], T]]) -> Callable[..., Union[Awaitable[T], T]]: - if sys.version_info.major == 2: - arg_names = inspect.getargspec(func).args - else: - arg_names = inspect.getfullargspec(func).args + def __call__( + self, func: t.Callable[..., t.Union[t.Awaitable[T], T]] + ) -> t.Callable[..., t.Union[t.Awaitable[T], T]]: + arg_names = inspect.getfullargspec(func).args params_to_provide = self._params if inspect.iscoroutinefunction(func): - @wraps(func) - async def async_injection_wrapper(*args: Any, **kwargs: Any) -> T: - provided_params = frozenset( - arg_names[:len(args)]) | frozenset(kwargs.keys()) + + @functools.wraps(func) + async def async_injection_wrapper(*args: t.Any, **kwargs: t.Any) -> T: # noqa: ANN401 + provided_params = frozenset(arg_names[: len(args)]) | frozenset( + kwargs.keys() + ) for param, cls in params_to_provide.items(): if param not in provided_params: kwargs[param] = instance(cls) - async_func = cast(Callable[..., Awaitable[T]], func) + async_func = t.cast("t.Callable[..., t.Awaitable[T]]", func) try: with contextlib.ExitStack() as sync_stack: async with contextlib.AsyncExitStack() as async_stack: - self._aggregate_sync_stack(sync_stack, provided_params, kwargs) - await self._aggregate_async_stack(async_stack, provided_params, kwargs) + self._aggregate_sync_stack( + sync_stack, provided_params, kwargs + ) + await self._aggregate_async_stack( + async_stack, provided_params, kwargs + ) return await async_func(*args, **kwargs) except TypeError as previous_error: - raise ConstructorTypeError(func, previous_error) + raise ConstructorTypeError(func, previous_error) # noqa: B904 return async_injection_wrapper - @wraps(func) - def injection_wrapper(*args: Any, **kwargs: Any) -> T: - provided_params = frozenset( - arg_names[:len(args)]) | frozenset(kwargs.keys()) + @functools.wraps(func) + def injection_wrapper(*args: t.Any, **kwargs: t.Any) -> T: # noqa: ANN401 + provided_params = frozenset(arg_names[: len(args)]) | frozenset( + kwargs.keys() + ) for param, cls in params_to_provide.items(): if param not in provided_params: kwargs[param] = instance(cls) - sync_func = cast(Callable[..., T], func) + sync_func = t.cast("t.Callable[..., T]", func) try: with contextlib.ExitStack() as sync_stack: self._aggregate_sync_stack(sync_stack, provided_params, kwargs) return sync_func(*args, **kwargs) except TypeError as previous_error: - raise ConstructorTypeError(func, previous_error) + raise ConstructorTypeError(func, previous_error) # noqa: B904 + return injection_wrapper def configure( - config: Optional[BinderCallable] = None, - bind_in_runtime: bool = True, - allow_override: bool = False, - clear: bool = False, - once: bool = False + config: t.Optional[BinderCallable] = None, + # TODO(pyctrl): force following flags to be kwargs + bind_in_runtime: bool = True, # noqa: FBT001, FBT002 + allow_override: bool = False, # noqa: FBT001, FBT002 + clear: bool = False, # noqa: FBT001, FBT002 + once: bool = False, # noqa: FBT001, FBT002 ) -> Injector: - """Create an injector with a callable config or raise an exception when already configured.""" - global _INJECTOR + """ + Create an injector using callable config. + + Raises: + InjectorException: if already configured. + + """ + global _INJECTOR # noqa: PLW0603 if clear and once: - raise InjectorException('clear and once are mutually exclusive, only one can be True') + msg = "clear and once are mutually exclusive, only one can be True" + raise InjectorException(msg) with _INJECTOR_LOCK: if _INJECTOR: @@ -457,41 +509,55 @@ def configure( elif once: return _INJECTOR else: - raise InjectorException('Injector is already configured') + raise InjectorException("Injector is already configured") - _INJECTOR = Injector(config, bind_in_runtime=bind_in_runtime, allow_override=allow_override) - logger.debug('Created and configured an injector, config=%s', config) + _INJECTOR = Injector( + config, + bind_in_runtime=bind_in_runtime, + allow_override=allow_override, + ) + logger.debug("Created and configured an injector, config=%s", config) return _INJECTOR def configure_once( - config: Optional[BinderCallable] = None, - bind_in_runtime: bool = True, - allow_override: bool = False + config: t.Optional[BinderCallable] = None, + # TODO(pyctrl): force following flags to be kwargs + bind_in_runtime: bool = True, # noqa: FBT001, FBT002 + allow_override: bool = False, # noqa: FBT001, FBT002 ) -> Injector: - """Create an injector with a callable config if not present, otherwise, do nothing. - + """ + Create an injector with a callable config if not present, otherwise, do nothing. + Deprecated, use `configure(once=True)` instead. """ with _INJECTOR_LOCK: if _INJECTOR: return _INJECTOR - return configure(config, bind_in_runtime=bind_in_runtime, allow_override=allow_override) + return configure( + config, bind_in_runtime=bind_in_runtime, allow_override=allow_override + ) def clear_and_configure( - config: Optional[BinderCallable] = None, - bind_in_runtime: bool = True, - allow_override: bool = False + config: t.Optional[BinderCallable] = None, + # TODO(pyctrl): force following flags to be kwargs + bind_in_runtime: bool = True, # noqa: FBT001, FBT002 + allow_override: bool = False, # noqa: FBT001, FBT002 ) -> Injector: - """Clear an existing injector and create another one with a callable config. - + """ + Clear an existing injector and create another one with a callable config. + Deprecated, use configure(clear=True) instead. """ with _INJECTOR_LOCK: _clear_injector() - return configure(config, bind_in_runtime=bind_in_runtime, allow_override=allow_override) + return configure( + config, + bind_in_runtime=bind_in_runtime, + allow_override=allow_override, + ) def is_configured() -> bool: @@ -507,34 +573,40 @@ def clear() -> None: def _clear_injector() -> None: """Clear an existing injector if present.""" - global _INJECTOR + global _INJECTOR # noqa: PLW0603 with _INJECTOR_LOCK: if _INJECTOR is None: return _INJECTOR = None - logger.debug('Cleared an injector') + logger.debug("Cleared an injector") -@overload -def instance(cls: Type[T]) -> T: ... +@t.overload +def instance(cls: type[T]) -> T: ... + + +@t.overload +def instance(cls: t.Hashable) -> Injectable: ... -@overload -def instance(cls: Hashable) -> Injectable: ... def instance(cls: Binding) -> Injectable: """Inject an instance of a class.""" return get_injector_or_die().get_instance(cls) -@overload + +@t.overload def attr() -> Injectable: ... -@overload -def attr(cls: Hashable) -> Injectable: ... -@overload -def attr(cls: Type[T]) -> T: ... +@t.overload +def attr(cls: t.Hashable) -> Injectable: ... + + +@t.overload +def attr(cls: type[T]) -> T: ... + def attr(cls=_MISSING): """Return an attribute injection (descriptor).""" @@ -545,13 +617,18 @@ def attr(cls=_MISSING): attr_dc = attr -def param(name: str, cls: Optional[Binding] = None) -> Callable: - """Deprecated, use @inject.params. Return a decorator which injects an arg into a function.""" +def param(name: str, cls: t.Optional[Binding] = None) -> t.Callable: + """ + Return a decorator which injects an arg into a function. + + Deprecated, use @inject.params. + """ return _ParameterInjection(name, cls) -def params(**args_to_classes: Binding) -> Callable: - """Return a decorator which injects args into a function. +def params(**args_to_classes: Binding) -> t.Callable: + """ + Return a decorator which injects args into a function. For example:: @@ -563,22 +640,27 @@ def sign_up(name, email, cache, db): # NOTE(pyctrl): only since 3.12 -# @overload -# def autoparams[T](fn: Callable[..., T]) -> Callable[..., T]: ... +# @t.overload +# def autoparams[T](fn: t.Callable[..., T]) -> t.Callable[..., T]: ... + + +@t.overload +def autoparams(fn: t.Callable) -> t.Callable: ... -@overload -def autoparams(fn: Callable) -> Callable: ... # NOTE(pyctrl): only since 3.12 -# @overload -# def autoparams[C: Callable](*selected: str) -> Callable[[C], C]: ... +# @t.overload +# def autoparams[C: t.Callable](*selected: str) -> t.Callable[[C], C]: + + +@t.overload +def autoparams(*selected: str) -> t.Callable: ... -@overload -def autoparams(*selected: str) -> Callable: ... -@no_type_check +@t.no_type_check def autoparams(*selected: str): - """Return a decorator that will inject args into a function using type annotations, Python >= 3.5 only. + """ + Return a decorator injecting args based on function type hints, only since 3.5. For example:: @@ -586,7 +668,8 @@ def autoparams(*selected: str): def refresh_cache(cache: RedisCache, db: DbInterface): pass - There is an option to specify which arguments we want to inject without attempts of injecting everything: + There is an option to specify which arguments we want to + inject without attempts of injecting everything: For example:: @@ -594,13 +677,13 @@ def refresh_cache(cache: RedisCache, db: DbInterface): def sign_up(name, email, cache: RedisCache, db: DbInterface): pass """ - only_these: Set[str] = set() + only_these: set[str] = set() - def autoparams_decorator(fn: Callable[..., T]) -> Callable[..., T]: + def autoparams_decorator(fn: t.Callable[..., T]) -> t.Callable[..., T]: if inspect.isclass(fn): - types = get_type_hints(fn.__init__) + types = t.get_type_hints(fn.__init__) else: - types = get_type_hints(fn) + types = t.get_type_hints(fn) # Skip the return annotation. types = {name: typ for name, typ in types.items() if name != _RETURN} @@ -611,7 +694,7 @@ def autoparams_decorator(fn: Callable[..., T]) -> Callable[..., T]: # Filter types if selected args present. if only_these: types = {name: typ for name, typ in types.items() if name in only_these} - + wrapper: _ParametersInjection[T] = _ParametersInjection(**types) return wrapper(fn) @@ -623,29 +706,41 @@ def autoparams_decorator(fn: Callable[..., T]) -> Callable[..., T]: return autoparams_decorator -def get_injector() -> Optional[Injector]: +def get_injector() -> t.Optional[Injector]: """Return the current injector or None.""" return _INJECTOR def get_injector_or_die() -> Injector: - """Return the current injector or raise an InjectorException.""" + """ + Return the current injector or raise an InjectorException. + + Raises: + InjectorException: If injector is not configured. + + Returns: + Configured injector. + + """ injector = _INJECTOR if not injector: - raise InjectorException('No injector is configured') + raise InjectorException("No injector is configured") return injector -def _unwrap_union_arg(typ): +def _unwrap_union_arg(typ: type) -> type: """Return the first type A in typing.Union[A, B] or typ if not Union.""" if not _is_union_type(typ): return typ return typ.__args__[0] -def _is_union_type(typ): - """Test if the type is a union type. Examples:: +def _is_union_type(typ: type) -> bool: + """ + Test if the type is a union type. + + Examples:: is_union_type(int) == False is_union_type(Union) == True is_union_type(Union[int, int]) == False @@ -654,21 +749,24 @@ def _is_union_type(typ): Source: https://github.com/ilevkivskyi/typing_inspect/blob/master/typing_inspect.py """ if _HAS_PEP604_SUPPORT: - return (typ is Union or - isinstance(typ, UnionType) or - isinstance(typ, _GenericAlias) and typ.__origin__ is Union) - elif _HAS_PEP560_SUPPORT: - return (typ is Union or - isinstance(typ, _GenericAlias) and typ.__origin__ is Union) + return ( + typ is t.Union + or isinstance(typ, UnionType) + or (isinstance(typ, _GenericAlias) and typ.__origin__ is t.Union) + ) + if _HAS_PEP560_SUPPORT: + return typ is t.Union or ( + isinstance(typ, _GenericAlias) and typ.__origin__ is t.Union + ) return type(typ) is _Union -def _unwrap_cls_annotation(cls: Type, attr_name: str): - types = get_type_hints(cls) +def _unwrap_cls_annotation(cls: type, attr_name: str) -> type: + types = t.get_type_hints(cls) try: attr_type = types[attr_name] except KeyError: msg = f"Couldn't find type annotation for {attr_name}" - raise InjectorException(msg) + raise InjectorException(msg) from None return _unwrap_union_arg(attr_type) diff --git a/tests/__init__.py b/tests/__init__.py index d4e0635..f372226 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,12 +1,14 @@ -from unittest import TestCase import asyncio +import typing as t +from unittest import TestCase + import inject class BaseTestInject(TestCase): - def tearDown(self): + def tearDown(self) -> None: inject.clear() - - def run_async(self, awaitable): + + def run_async(self, awaitable: t.Awaitable): # noqa: ANN201 loop = asyncio.get_event_loop() - return loop.run_until_complete(awaitable) \ No newline at end of file + return loop.run_until_complete(awaitable) diff --git a/tests/test_attr.py b/tests/test_attr.py index 5411b30..cedfc8e 100644 --- a/tests/test_attr.py +++ b/tests/test_attr.py @@ -1,13 +1,14 @@ -from dataclasses import dataclass +import dataclasses + import inject from tests import BaseTestInject class TestInjectAttr(BaseTestInject): def test_attr(self): - @dataclass + @dataclasses.dataclass class MyDataClass: - field = inject.attr(int) + field = inject.attr(int) # noqa: RUF045 class MyClass: field = inject.attr(int) @@ -28,7 +29,7 @@ class MyClass: assert my_dc.field == 123 def test_invalid_attachment_to_dataclass(self): - @dataclass + @dataclasses.dataclass class MyDataClass: # dataclasses treat this definition as regular descriptor field: int = inject.attr(int) @@ -39,11 +40,11 @@ def test_class_attr(self): descriptor = inject.attr(int) auto_descriptor = inject.attr() - @dataclass + @dataclasses.dataclass class MyDataClass: - field = descriptor + field = descriptor # noqa: RUF045 - class MyClass(object): + class MyClass: field = descriptor field2: int = descriptor auto_typed_field: int = auto_descriptor diff --git a/tests/test_autoparams.py b/tests/test_autoparams.py index a3239c9..03775a2 100644 --- a/tests/test_autoparams.py +++ b/tests/test_autoparams.py @@ -1,14 +1,20 @@ import sys -from typing import Optional +import typing as t +import inject from tests import BaseTestInject -import inject +class A: + pass + + +class B: + pass -class A: pass -class B: pass -class C: pass + +class C: + pass class TestInjectEmptyAutoparamsWithBraces(BaseTestInject): @@ -18,7 +24,7 @@ def _get_decorator(): def test_autoparams_by_class(self): @self._get_decorator() - def test_func(val: int = None): + def test_func(val: t.Optional[int] = None): return val inject.configure(lambda binder: binder.bind(int, 123)) @@ -42,16 +48,16 @@ def config(binder): assert test_func(10) == (10, 2, 3) assert test_func(10, 20) == (10, 20, 3) assert test_func(10, 20, c=30) == (10, 20, 30) - assert test_func(a='a') == ('a', 2, 3) - assert test_func(b='b') == (1, 'b', 3) - assert test_func(c='c') == (1, 2, 'c') + assert test_func(a="a") == ("a", 2, 3) + assert test_func(b="b") == (1, "b", 3) + assert test_func(c="c") == (1, 2, "c") assert test_func(a=10, c=30) == (10, 2, 30) assert test_func(c=30, b=20, a=10) == (10, 20, 30) assert test_func(10, b=20) == (10, 20, 3) def test_autoparams_strings(self): @self._get_decorator() - def test_func(a: 'A', b: 'B', *, c: 'C'): + def test_func(a: A, b: B, *, c: C): return a, b, c def config(binder): @@ -65,16 +71,16 @@ def config(binder): assert test_func(10) == (10, 2, 3) assert test_func(10, 20) == (10, 20, 3) assert test_func(10, 20, c=30) == (10, 20, 30) - assert test_func(a='a') == ('a', 2, 3) - assert test_func(b='b') == (1, 'b', 3) - assert test_func(c='c') == (1, 2, 'c') + assert test_func(a="a") == ("a", 2, 3) + assert test_func(b="b") == (1, "b", 3) + assert test_func(c="c") == (1, 2, "c") assert test_func(a=10, c=30) == (10, 2, 30) assert test_func(c=30, b=20, a=10) == (10, 20, 30) assert test_func(10, b=20) == (10, 20, 3) def test_autoparams_with_defaults(self): @self._get_decorator() - def test_func(a=1, b: 'B' = None, *, c: 'C' = 300): + def test_func(a=1, b: B = None, *, c: C = 300): return a, b, c def config(binder): @@ -87,9 +93,9 @@ def config(binder): assert test_func(10) == (10, 2, 3) assert test_func(10, 20) == (10, 20, 3) assert test_func(10, 20, c=30) == (10, 20, 30) - assert test_func(a='a') == ('a', 2, 3) - assert test_func(b='b') == (1, 'b', 3) - assert test_func(c='c') == (1, 2, 'c') + assert test_func(a="a") == ("a", 2, 3) + assert test_func(b="b") == (1, "b", 3) + assert test_func(c="c") == (1, 2, "c") assert test_func(a=10, c=30) == (10, 2, 30) assert test_func(c=30, b=20, a=10) == (10, 20, 30) assert test_func(10, b=20) == (10, 20, 3) @@ -97,7 +103,7 @@ def config(binder): def test_autoparams_on_method(self): class Test: @self._get_decorator() - def func(self, a=1, b: 'B' = None, *, c: 'C' = None): + def func(self, a=1, b: B = None, *, c: t.Optional[C] = None): return self, a, b, c def config(binder): @@ -111,9 +117,9 @@ def config(binder): assert test.func(10) == (test, 10, 2, 3) assert test.func(10, 20) == (test, 10, 20, 3) assert test.func(10, 20, c=30) == (test, 10, 20, 30) - assert test.func(a='a') == (test, 'a', 2, 3) - assert test.func(b='b') == (test, 1, 'b', 3) - assert test.func(c='c') == (test, 1, 2, 'c') + assert test.func(a="a") == (test, "a", 2, 3) + assert test.func(b="b") == (test, 1, "b", 3) + assert test.func(c="c") == (test, 1, 2, "c") assert test.func(a=10, c=30) == (test, 10, 2, 30) assert test.func(c=30, b=20, a=10) == (test, 10, 20, 30) assert test.func(10, b=20) == (test, 10, 20, 3) @@ -123,7 +129,7 @@ class Test: # note inject must be *before* classmethod! @classmethod @self._get_decorator() - def func(cls, a=1, b: 'B' = None, *, c: 'C' = None): + def func(cls, a=1, b: B = None, *, c: t.Optional[C] = None): return cls, a, b, c def config(binder): @@ -136,9 +142,9 @@ def config(binder): assert Test.func(10) == (Test, 10, 2, 3) assert Test.func(10, 20) == (Test, 10, 20, 3) assert Test.func(10, 20, c=30) == (Test, 10, 20, 30) - assert Test.func(a='a') == (Test, 'a', 2, 3) - assert Test.func(b='b') == (Test, 1, 'b', 3) - assert Test.func(c='c') == (Test, 1, 2, 'c') + assert Test.func(a="a") == (Test, "a", 2, 3) + assert Test.func(b="b") == (Test, 1, "b", 3) + assert Test.func(c="c") == (Test, 1, 2, "c") assert Test.func(a=10, c=30) == (Test, 10, 2, 30) assert Test.func(c=30, b=20, a=10) == (Test, 10, 20, 30) assert Test.func(10, b=20) == (Test, 10, 20, 3) @@ -148,7 +154,7 @@ class Test: # note inject must be *before* classmethod! @classmethod @self._get_decorator() - def func(cls, a=1, b: 'B' = None, *, c: 'C' = None): + def func(cls, a=1, b: B = None, *, c: t.Optional[C] = None): return cls, a, b, c def config(binder): @@ -162,9 +168,9 @@ def config(binder): assert test.func(10) == (Test, 10, 2, 3) assert test.func(10, 20) == (Test, 10, 20, 3) assert test.func(10, 20, c=30) == (Test, 10, 20, 30) - assert test.func(a='a') == (Test, 'a', 2, 3) - assert test.func(b='b') == (Test, 1, 'b', 3) - assert test.func(c='c') == (Test, 1, 2, 'c') + assert test.func(a="a") == (Test, "a", 2, 3) + assert test.func(b="b") == (Test, 1, "b", 3) + assert test.func(c="c") == (Test, 1, 2, "c") assert test.func(a=10, c=30) == (Test, 10, 2, 30) assert test.func(c=30, b=20, a=10) == (Test, 10, 20, 30) assert test.func(10, b=20) == (Test, 10, 20, 3) @@ -175,11 +181,11 @@ def test_func(a: str) -> int: return a def config(binder): - binder.bind(str, 'bazinga') + binder.bind(str, "bazinga") inject.configure(config) - assert test_func() == 'bazinga' + assert test_func() == "bazinga" class TestInjectEmptyAutoparamsNoBraces(TestInjectEmptyAutoparamsWithBraces): @@ -189,10 +195,9 @@ def _get_decorator(): class TestInjectSelectedAutoparams(BaseTestInject): - def test_autoparams_only_selected(self): - @inject.autoparams('a', 'c') - def test_func(a: 'A', b: 'B', *, c: 'C'): + @inject.autoparams("a", "c") + def test_func(a: A, b: B, *, c: C): return a, b, c def config(binder): @@ -206,8 +211,8 @@ def config(binder): self.assertRaises(TypeError, test_func, a=1, c=3) def test_autoparams_only_selected_with_optional(self): - @inject.autoparams('a', 'c') - def test_func(a: 'A', b: 'B', *, c: Optional[C] = None): + @inject.autoparams("a", "c") + def test_func(a: A, b: B, *, c: t.Optional[C] = None): return a, b, c def config(binder): @@ -224,8 +229,8 @@ def test_autoparams_only_selected_with_optional_pep604_union(self): if not sys.version_info[:3] >= (3, 10, 0): return - @inject.autoparams('a', 'c') - def test_func(a: 'A', b: 'B', *, c: C | None = None): + @inject.autoparams("a", "c") + def test_func(a: A, b: B, *, c: t.Optional[C] = None): return a, b, c def config(binder): diff --git a/tests/test_binder.py b/tests/test_binder.py index 9decd2a..bd7e9d1 100644 --- a/tests/test_binder.py +++ b/tests/test_binder.py @@ -1,50 +1,74 @@ from unittest import TestCase -from inject import Binder, InjectorException +import inject class TestBinder(TestCase): def test_bind(self): - binder = Binder() + binder = inject.Binder() binder.bind(int, 123) assert int in binder._bindings def test_bind__class_required(self): - binder = Binder() + binder = inject.Binder() - self.assertRaisesRegex(InjectorException, 'Binding key cannot be None', binder.bind, None, None) + self.assertRaisesRegex( + inject.InjectorException, + "Binding key cannot be None", + binder.bind, + None, + None, + ) def test_bind__duplicate_binding(self): - binder = Binder() + binder = inject.Binder() binder.bind(int, 123) - self.assertRaisesRegex(InjectorException, "Duplicate binding", binder.bind, int, 456) + self.assertRaisesRegex( + inject.InjectorException, + "Duplicate binding", + binder.bind, + int, + 456, + ) def test_bind__allow_override(self): - binder = Binder(allow_override=True) + binder = inject.Binder(allow_override=True) binder.bind(int, 123) binder.bind(int, 456) assert int in binder._bindings def test_bind_provider(self): provider = lambda: 123 - binder = Binder() + binder = inject.Binder() binder.bind_to_provider(int, provider) assert binder._bindings[int] is provider def test_bind_provider__provider_required(self): - binder = Binder() - self.assertRaisesRegex(InjectorException, "Provider cannot be None", binder.bind_to_provider, int, None) + binder = inject.Binder() + self.assertRaisesRegex( + inject.InjectorException, + "Provider cannot be None", + binder.bind_to_provider, + int, + None, + ) def test_bind_constructor(self): constructor = lambda: 123 - binder = Binder() + binder = inject.Binder() binder.bind_to_constructor(int, constructor) assert binder._bindings[int]._constructor is constructor def test_bind_constructor__constructor_required(self): - binder = Binder() - self.assertRaisesRegex(InjectorException, "Constructor cannot be None", binder.bind_to_constructor, int, None) + binder = inject.Binder() + self.assertRaisesRegex( + inject.InjectorException, + "Constructor cannot be None", + binder.bind_to_constructor, + int, + None, + ) diff --git a/tests/test_context_manager.py b/tests/test_context_manager.py index 058bf52..9b19bc8 100644 --- a/tests/test_context_manager.py +++ b/tests/test_context_manager.py @@ -12,16 +12,13 @@ def destroy(self): self.started = False -class MockFile(Destroyable): - ... +class MockFile(Destroyable): ... -class MockConnection(Destroyable): - ... +class MockConnection(Destroyable): ... -class MockFoo(Destroyable): - ... +class MockFoo(Destroyable): ... @contextlib.contextmanager @@ -46,21 +43,20 @@ def get_foo_sync(): @contextlib.asynccontextmanager -async def get_file_async(): +async def get_file_async(): # noqa: RUF029 obj = MockFile() yield obj obj.destroy() @contextlib.asynccontextmanager -async def get_conn_async(): +async def get_conn_async(): # noqa: RUF029 obj = MockConnection() yield obj obj.destroy() class TestContextManagerFunctional(BaseTestInject): - def test_provider_as_context_manager_sync(self): def config(binder): binder.bind_to_provider(MockFile, get_file_sync) @@ -93,7 +89,13 @@ def config(binder): inject.configure(config) @inject.autoparams() - async def mock_func(conn: MockConnection, name: str, f: MockFile, number: int, foo: MockFoo): + async def mock_func( # noqa: RUF029 + conn: MockConnection, + name: str, + f: MockFile, + number: int, + foo: MockFoo, + ): assert f.started assert conn.started assert foo.started @@ -104,4 +106,4 @@ async def mock_func(conn: MockConnection, name: str, f: MockFile, number: int, f f_, conn_, foo_ = self.run_async(mock_func()) assert not f_.started assert not conn_.started - assert not foo_.started \ No newline at end of file + assert not foo_.started diff --git a/tests/test_functional.py b/tests/test_functional.py index 7377154..eae7356 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -1,7 +1,7 @@ +import dataclasses from unittest import TestCase import inject -from inject import autoparams, ConstructorTypeError class TestFunctional(TestCase): @@ -9,43 +9,38 @@ def tearDown(self): inject.clear() def test(self): - class Config(object): + class Config: def __init__(self, greeting): self.greeting = greeting - class Cache(object): + class Cache: config = inject.attr(Config) def load_greeting(self): return self.config.greeting - class User(object): + class User: cache = inject.attr(Cache) def __init__(self, name): self.name = name def greet(self): - return '%s, %s' % (self.cache.load_greeting(), self.name) + return f"{self.cache.load_greeting()}, {self.name}" def config(binder): - binder.bind(Config, Config('Hello')) + binder.bind(Config, Config("Hello")) inject.configure(config) - user = User('John Doe') + user = User("John Doe") greeting = user.greet() - assert greeting == 'Hello, John Doe' + assert greeting == "Hello, John Doe" def test_class_with_restricted_bool_casting(self): - class DataFrame(object): - def __nonzero__(self): - """Python 2""" - raise NotImplementedError('Casting to boolean is not allowed') - + class DataFrame: def __bool__(self): - """Python 3""" - raise NotImplementedError('Casting to boolean is not allowed') + raise NotImplementedError("Casting to boolean is not allowed") def create_data_frame(): return DataFrame() @@ -63,13 +58,13 @@ def test_class_support_in_autoparams_programmaticaly(self): class AnotherClass: pass + @dataclasses.dataclass class SomeClass: - def __init__(self, another_object: AnotherClass): - self.another_object = another_object + another_object: AnotherClass def config(binder): - binder.bind_to_constructor(SomeClass, autoparams()(SomeClass)) - binder.bind_to_constructor(AnotherClass, autoparams()(AnotherClass)) + binder.bind_to_constructor(SomeClass, inject.autoparams()(SomeClass)) + binder.bind_to_constructor(AnotherClass, inject.autoparams()(AnotherClass)) inject.configure(config) @@ -89,7 +84,7 @@ def create_some_class(missing_arg): return AnotherClass(missing_arg) def config(binder): - binder.bind_to_constructor(SomeClass, autoparams()(SomeClass)) + binder.bind_to_constructor(SomeClass, inject.autoparams()(SomeClass)) binder.bind_to_constructor(AnotherClass, create_some_class) inject.configure() @@ -97,22 +92,22 @@ def config(binder): # covers case when no constructor provided try: inject.instance(SomeClass) - except ConstructorTypeError as err: - assert 'SomeClass' in str(err) - assert 'missing_arg' in str(err) + except inject.ConstructorTypeError as err: + assert "SomeClass" in str(err) + assert "missing_arg" in str(err) inject.clear_and_configure(config) # covers case with provided constructor try: inject.instance(SomeClass) - except ConstructorTypeError as err: - assert 'SomeClass' in str(err) - assert 'missing_arg' in str(err) + except inject.ConstructorTypeError as err: + assert "SomeClass" in str(err) + assert "missing_arg" in str(err) try: inject.instance(AnotherClass) except TypeError as err: - assert not isinstance(err, ConstructorTypeError) - assert 'create_some_class' in str(err) - assert 'missing_arg' in str(err) \ No newline at end of file + assert not isinstance(err, inject.ConstructorTypeError) + assert "create_some_class" in str(err) + assert "missing_arg" in str(err) diff --git a/tests/test_inject_configuration.py b/tests/test_inject_configuration.py index 7ea10f1..573ec26 100644 --- a/tests/test_inject_configuration.py +++ b/tests/test_inject_configuration.py @@ -1,5 +1,4 @@ import inject -from inject import InjectorException from tests import BaseTestInject @@ -18,7 +17,11 @@ def test_configure__should_add_bindings(self): def test_configure__already_configured(self): inject.configure() - self.assertRaisesRegex(InjectorException, 'Injector is already configured', inject.configure) + self.assertRaisesRegex( + inject.InjectorException, + "Injector is already configured", + inject.configure, + ) def test_configure_once__should_create_injector(self): injector = inject.configure_once() @@ -46,11 +49,20 @@ def test_clear_and_configure(self): assert injector1 is not injector0 def test_get_injector_or_die(self): - self.assertRaisesRegex(InjectorException, 'No injector is configured', inject.get_injector_or_die) + self.assertRaisesRegex( + inject.InjectorException, + "No injector is configured", + inject.get_injector_or_die, + ) def test_configure__runtime_binding_disabled(self): injector = inject.configure(bind_in_runtime=False) - self.assertRaisesRegex(InjectorException, "No binding was found for key=<.* 'int'>", injector.get_instance, int) + self.assertRaisesRegex( + inject.InjectorException, + "No binding was found for key=<.* 'int'>", + injector.get_instance, + int, + ) def test_configure__install_allow_override(self): def base_config(binder): diff --git a/tests/test_injector.py b/tests/test_injector.py index 59335f0..fea7e10 100644 --- a/tests/test_injector.py +++ b/tests/test_injector.py @@ -1,34 +1,37 @@ -from random import random +import random from unittest import TestCase -from inject import Injector, InjectorException +import inject class TestInjector(TestCase): def test_instance_binding__should_use_the_same_instance(self): - injector = Injector(lambda binder: binder.bind(int, 123)) + injector = inject.Injector(lambda binder: binder.bind(int, 123)) instance = injector.get_instance(int) assert instance == 123 def test_constructor_binding__should_construct_singleton(self): - injector = Injector(lambda binder: binder.bind_to_constructor(int, random)) + injector = inject.Injector( + lambda binder: binder.bind_to_constructor(int, random.random) + ) instance0 = injector.get_instance(int) instance1 = injector.get_instance(int) assert instance0 == instance1 def test_provider_binding__should_call_provider_for_each_injection(self): - injector = Injector(lambda binder: binder.bind_to_provider(int, random)) + injector = inject.Injector( + lambda binder: binder.bind_to_provider(int, random.random) + ) instance0 = injector.get_instance(int) instance1 = injector.get_instance(int) assert instance0 != instance1 - def test_runtime_binding__should_create_runtime_singleton(self): - class MyClass(object): + class MyClass: pass - injector = Injector() + injector = inject.Injector() instance0 = injector.get_instance(MyClass) instance1 = injector.get_instance(MyClass) @@ -36,13 +39,19 @@ class MyClass(object): assert isinstance(instance0, MyClass) def test_runtime_binding__not_callable(self): - injector = Injector() - self.assertRaisesRegex(InjectorException, - 'Cannot create a runtime binding, the key is not callable, key=123', - injector.get_instance, 123) + injector = inject.Injector() + self.assertRaisesRegex( + inject.InjectorException, + "Cannot create a runtime binding, the key is not callable, key=123", + injector.get_instance, + 123, + ) def test_runtime_binding__disabled(self): - injector = Injector(bind_in_runtime=False) - self.assertRaisesRegex(InjectorException, - "No binding was found for key=<.* 'int'>", - injector.get_instance, int) + injector = inject.Injector(bind_in_runtime=False) + self.assertRaisesRegex( + inject.InjectorException, + "No binding was found for key=<.* 'int'>", + injector.get_instance, + int, + ) diff --git a/tests/test_param.py b/tests/test_param.py index 29767a7..a8c9593 100644 --- a/tests/test_param.py +++ b/tests/test_param.py @@ -1,34 +1,35 @@ +import inspect + import inject from tests import BaseTestInject -import inspect -import asyncio + class TestInjectParams(BaseTestInject): def test_param_by_name(self): - @inject.param('val') + @inject.param("val") def test_func(val=None): return val - inject.configure(lambda binder: binder.bind('val', 123)) + inject.configure(lambda binder: binder.bind("val", 123)) assert test_func() == 123 assert test_func(val=321) == 321 def test_param_by_class(self): - @inject.param('val', int) + @inject.param("val", int) def test_func(val): return val inject.configure(lambda binder: binder.bind(int, 123)) assert test_func() == 123 - + def test_async_param(self): - @inject.param('val') - async def test_func(val): + @inject.param("val") + async def test_func(val): # noqa: RUF029 return val - - inject.configure(lambda binder: binder.bind('val', 123)) + + inject.configure(lambda binder: binder.bind("val", 123)) assert inspect.iscoroutinefunction(test_func) assert self.run_async(test_func()) == 123 diff --git a/tests/test_params.py b/tests/test_params.py index b6ff9de..cf5ad95 100644 --- a/tests/test_params.py +++ b/tests/test_params.py @@ -1,7 +1,8 @@ +import inspect + import inject from tests import BaseTestInject -import inspect -import asyncio + class TestInjectParams(BaseTestInject): def test_params(self): @@ -16,14 +17,14 @@ def test_func(val): assert test_func(val=42) == 42 def test_params_multi(self): - @inject.params(a='A', b='B', c='C') + @inject.params(a="A", b="B", c="C") def test_func(a, b, c): return a, b, c def config(binder): - binder.bind('A', 1) - binder.bind('B', 2) - binder.bind('C', 3) + binder.bind("A", 1) + binder.bind("B", 2) + binder.bind("C", 3) inject.configure(config) @@ -31,22 +32,22 @@ def config(binder): assert test_func(10) == (10, 2, 3) assert test_func(10, 20) == (10, 20, 3) assert test_func(10, 20, 30) == (10, 20, 30) - assert test_func(a='a') == ('a', 2, 3) - assert test_func(b='b') == (1, 'b', 3) - assert test_func(c='c') == (1, 2, 'c') + assert test_func(a="a") == ("a", 2, 3) + assert test_func(b="b") == (1, "b", 3) + assert test_func(c="c") == (1, 2, "c") assert test_func(a=10, c=30) == (10, 2, 30) assert test_func(c=30, b=20, a=10) == (10, 20, 30) assert test_func(10, b=20) == (10, 20, 3) def test_params_with_defaults(self): # note the inject overrides default parameters - @inject.params(b='B', c='C') + @inject.params(b="B", c="C") def test_func(a=1, b=None, c=300): return a, b, c def config(binder): - binder.bind('B', 2) - binder.bind('C', 3) + binder.bind("B", 2) + binder.bind("C", 3) inject.configure(config) @@ -54,22 +55,22 @@ def config(binder): assert test_func(10) == (10, 2, 3) assert test_func(10, 20) == (10, 20, 3) assert test_func(10, 20, 30) == (10, 20, 30) - assert test_func(a='a') == ('a', 2, 3) - assert test_func(b='b') == (1, 'b', 3) - assert test_func(c='c') == (1, 2, 'c') + assert test_func(a="a") == ("a", 2, 3) + assert test_func(b="b") == (1, "b", 3) + assert test_func(c="c") == (1, 2, "c") assert test_func(a=10, c=30) == (10, 2, 30) assert test_func(c=30, b=20, a=10) == (10, 20, 30) assert test_func(10, b=20) == (10, 20, 3) def test_params_on_method(self): class Test: - @inject.params(b='B', c='C') + @inject.params(b="B", c="C") def func(self, a=1, b=None, c=None): return self, a, b, c def config(binder): - binder.bind('B', 2) - binder.bind('C', 3) + binder.bind("B", 2) + binder.bind("C", 3) inject.configure(config) test = Test() @@ -78,9 +79,9 @@ def config(binder): assert test.func(10) == (test, 10, 2, 3) assert test.func(10, 20) == (test, 10, 20, 3) assert test.func(10, 20, 30) == (test, 10, 20, 30) - assert test.func(a='a') == (test, 'a', 2, 3) - assert test.func(b='b') == (test, 1, 'b', 3) - assert test.func(c='c') == (test, 1, 2, 'c') + assert test.func(a="a") == (test, "a", 2, 3) + assert test.func(b="b") == (test, 1, "b", 3) + assert test.func(c="c") == (test, 1, 2, "c") assert test.func(a=10, c=30) == (test, 10, 2, 30) assert test.func(c=30, b=20, a=10) == (test, 10, 20, 30) assert test.func(10, b=20) == (test, 10, 20, 3) @@ -89,13 +90,13 @@ def test_params_on_classmethod(self): class Test: # note inject must be *before* classmethod! @classmethod - @inject.params(b='B', c='C') + @inject.params(b="B", c="C") def func(cls, a=1, b=None, c=None): return cls, a, b, c def config(binder): - binder.bind('B', 2) - binder.bind('C', 3) + binder.bind("B", 2) + binder.bind("C", 3) inject.configure(config) @@ -103,9 +104,9 @@ def config(binder): assert Test.func(10) == (Test, 10, 2, 3) assert Test.func(10, 20) == (Test, 10, 20, 3) assert Test.func(10, 20, 30) == (Test, 10, 20, 30) - assert Test.func(a='a') == (Test, 'a', 2, 3) - assert Test.func(b='b') == (Test, 1, 'b', 3) - assert Test.func(c='c') == (Test, 1, 2, 'c') + assert Test.func(a="a") == (Test, "a", 2, 3) + assert Test.func(b="b") == (Test, 1, "b", 3) + assert Test.func(c="c") == (Test, 1, 2, "c") assert Test.func(a=10, c=30) == (Test, 10, 2, 30) assert Test.func(c=30, b=20, a=10) == (Test, 10, 20, 30) assert Test.func(10, b=20) == (Test, 10, 20, 3) @@ -114,13 +115,13 @@ def test_params_on_classmethod_ob_object(self): class Test: # note inject must be *before* classmethod! @classmethod - @inject.params(b='B', c='C') + @inject.params(b="B", c="C") def func(cls, a=1, b=None, c=None): return cls, a, b, c def config(binder): - binder.bind('B', 2) - binder.bind('C', 3) + binder.bind("B", 2) + binder.bind("C", 3) inject.configure(config) test = Test @@ -129,16 +130,16 @@ def config(binder): assert test.func(10) == (Test, 10, 2, 3) assert test.func(10, 20) == (Test, 10, 20, 3) assert test.func(10, 20, 30) == (Test, 10, 20, 30) - assert test.func(a='a') == (Test, 'a', 2, 3) - assert test.func(b='b') == (Test, 1, 'b', 3) - assert test.func(c='c') == (Test, 1, 2, 'c') + assert test.func(a="a") == (Test, "a", 2, 3) + assert test.func(b="b") == (Test, 1, "b", 3) + assert test.func(c="c") == (Test, 1, 2, "c") assert test.func(a=10, c=30) == (Test, 10, 2, 30) assert test.func(c=30, b=20, a=10) == (Test, 10, 20, 30) assert test.func(10, b=20) == (Test, 10, 20, 3) def test_async_params(self): @inject.params(val=int) - async def test_func(val): + async def test_func(val): # noqa: RUF029 return val inject.configure(lambda binder: binder.bind(int, 123))