diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a7cb052..de81049 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v1 @@ -69,25 +69,20 @@ jobs: path: junit/pytest-results-${{ matrix.python-version }}.xml if: always() - - name: Codecov - run: | - bash <(sed -i 's/filename=\"/filename=\"rodi\//g' coverage.xml) - bash <(curl -s https://codecov.io/bash) - - name: Install distribution dependencies run: pip install --upgrade build - if: matrix.python-version == 3.12 + if: matrix.python-version == 3.13 - name: Create distribution package run: python -m build - if: matrix.python-version == 3.12 + if: matrix.python-version == 3.13 - name: Upload distribution package uses: actions/upload-artifact@v4 with: name: dist path: dist - if: matrix.python-version == 3.12 + if: matrix.python-version == 3.13 publish: runs-on: ubuntu-latest @@ -100,10 +95,10 @@ jobs: name: dist path: dist - - name: Use Python 3.12 + - name: Use Python 3.13 uses: actions/setup-python@v1 with: - python-version: '3.12' + python-version: '3.13' - name: Install dependencies run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f46b23..ec77ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.1.0] - 2025-11-24 :notes: + +- Drop support for Python <= 3.10. +- Add Python 3.14 to the build matrix and to classifiers. +- Remove Codecov from GitHub Workflow and from README. +- Upgrade type annotations to Python >= 3.10. +- Remove code checks for Python <= 3.10. + ## [2.0.8] - 2025-04-12 - Add the link to the [documentation](https://www.neoteroi.dev/rodi/). diff --git a/pyproject.toml b/pyproject.toml index 7f3e115..dd29f71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,21 +9,19 @@ authors = [{ name = "Roberto Prevato", email = "roberto.prevato@gmail.com" }] description = "Implementation of dependency injection for Python 3" license = { file = "LICENSE" } readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", ] keywords = ["dependency", "injection", "type", "hints", "typing"] -dependencies = ["typing_extensions; python_version < '3.8'"] +dependencies = [] [tool.hatch.build.targets.sdist] exclude = [ @@ -47,6 +45,9 @@ exclude = [ "examples-summary.py", ] +[tool.hatch.build.targets.wheel] +packages = ["rodi"] + [tool.hatch.version] path = "rodi/__about__.py" diff --git a/rodi/__init__.py b/rodi/__init__.py index 1262a00..f5812c5 100644 --- a/rodi/__init__.py +++ b/rodi/__init__.py @@ -10,29 +10,17 @@ Callable, ClassVar, DefaultDict, - Dict, Mapping, - Optional, - Set, + Protocol, Type, TypeVar, - Union, +) +from typing import _no_init_or_replace_init as _no_init +from typing import ( cast, get_type_hints, ) -if sys.version_info >= (3, 8): # pragma: no cover - try: - from typing import _no_init_or_replace_init as _no_init - except ImportError: # pragma: no cover - from typing import _no_init - -try: - from typing import Protocol -except ImportError: # pragma: no cover - from typing_extensions import Protocol - - T = TypeVar("T") @@ -42,10 +30,10 @@ class ContainerProtocol(Protocol): and tell if a type is configured. """ - def register(self, obj_type: Union[Type, str], *args, **kwargs): + def register(self, obj_type: Type | str, *args, **kwargs): """Registers a type in the container, with optional arguments.""" - def resolve(self, obj_type: Union[Type[T], str], *args, **kwargs) -> T: + def resolve(self, obj_type: Type[T] | str, *args, **kwargs) -> T: """Activates an instance of the given type, with optional arguments.""" def __contains__(self, item) -> bool: @@ -54,7 +42,7 @@ def __contains__(self, item) -> bool: """ -AliasesTypeHint = Dict[str, Type] +AliasesTypeHint = dict[str, Type] def inject(globalsns=None, localns=None) -> Callable[..., Any]: @@ -81,7 +69,7 @@ def decorator(f): return decorator -def _get_obj_locals(obj) -> Optional[Dict[str, Any]]: +def _get_obj_locals(obj) -> dict[str, Any] | None: return getattr(obj, "_locals", None) @@ -227,8 +215,8 @@ class ActivationScope: def __init__( self, - provider: Optional["Services"] = None, - scoped_services: Optional[Dict[Union[Type[T], str], T]] = None, + provider: "Services | None" = None, + scoped_services: dict[Type[T] | str, T] | None = None, ): self.provider = provider or Services() self.scoped_services = scoped_services or {} @@ -243,10 +231,10 @@ def __exit__(self, exc_type, exc_val, exc_tb): def get( self, - desired_type: Union[Type[T], str], - scope: Optional["ActivationScope"] = None, + desired_type: Type[T] | str, + scope: "ActivationScope | None" = None, *, - default: Optional[Any] = ..., + default: Any = ..., ) -> T: if self.provider is None: raise TypeError("This scope is disposed.") @@ -569,16 +557,14 @@ def _resolve_by_init_method(self, context: ResolutionContext): for key, value in sig.parameters.items() } - if sys.version_info >= (3, 10): # pragma: no cover - # Python 3.10 - annotations = get_type_hints( - self.concrete_type.__init__, - vars(sys.modules[self.concrete_type.__module__]), - _get_obj_locals(self.concrete_type), - ) - for key, value in params.items(): - if key in annotations: - value.annotation = annotations[key] + annotations = get_type_hints( + self.concrete_type.__init__, + vars(sys.modules[self.concrete_type.__module__]), + _get_obj_locals(self.concrete_type), + ) + for key, value in params.items(): + if key in annotations: + value.annotation = annotations[key] concrete_type = self.concrete_type @@ -618,13 +604,12 @@ def _has_default_init(self): if init is object.__init__: return True - if sys.version_info >= (3, 8): # pragma: no cover - if init is _no_init: - return True + if init is _no_init: + return True return False def _resolve_by_annotations( - self, context: ResolutionContext, annotations: Dict[str, Type] + self, context: ResolutionContext, annotations: dict[str, Type] ): params = { key: Dependency(key, value) @@ -711,7 +696,7 @@ class Services: def __init__( self, services_map=None, - scope_cls: Optional[Type[ActivationScope]] = None, + scope_cls: Type[ActivationScope] | None = None, ): if services_map is None: services_map = {} @@ -729,11 +714,11 @@ def __setitem__(self, key, value): self.set(key, value) def create_scope( - self, scoped: Optional[Dict[Union[Type, str], Any]] = None + self, scoped: dict[Type | str, Any] | None = None ) -> ActivationScope: return self._scope_cls(self, scoped) - def set(self, new_type: Union[Type, str], value: Any): + def set(self, new_type: Type | str, value: Any): """ Sets a new service of desired type, as singleton. This method exists to increase interoperability of Services class (with dict). @@ -757,10 +742,10 @@ def resolver(context, desired_type): def get( self, - desired_type: Union[Type[T], str], - scope: Optional[ActivationScope] = None, + desired_type: Type[T] | str, + scope: ActivationScope | None = None, *, - default: Optional[Any] = ..., + default: Any = ..., ) -> T: """ Gets a service of the desired type, returning an activated instance. @@ -803,12 +788,10 @@ def get_executor(self, method: Callable) -> Callable: for key, value in sig.parameters.items() } - if sys.version_info >= (3, 10): # pragma: no cover - # Python 3.10 - annotations = _get_factory_annotations_or_throw(method) - for key, value in params.items(): - if key in annotations: - value.annotation = annotations[key] + annotations = _get_factory_annotations_or_throw(method) + for key, value in params.items(): + if key in annotations: + value.annotation = annotations[key] fns = [] @@ -818,14 +801,14 @@ def get_executor(self, method: Callable) -> Callable: if iscoroutinefunction(method): async def async_executor( - scoped: Optional[Dict[Union[Type, str], Any]] = None, + scoped: dict[Type | str, Any] | None = None, ): with self.create_scope(scoped) as context: return await method(*[fn(context) for fn in fns]) return async_executor - def executor(scoped: Optional[Dict[Union[Type, str], Any]] = None): + def executor(scoped: dict[Type | str, Any] | None = None): with self.create_scope(scoped) as context: return method(*[fn(context) for fn in fns]) @@ -834,7 +817,7 @@ def executor(scoped: Optional[Dict[Union[Type, str], Any]] = None): def exec( self, method: Callable, - scoped: Optional[Dict[Type, Any]] = None, + scoped: dict[Type, Any] | None = None, ) -> Any: try: executor = self._executors[method] @@ -847,11 +830,11 @@ def exec( FactoryCallableNoArguments = Callable[[], Any] FactoryCallableSingleArgument = Callable[[ActivationScope], Any] FactoryCallableTwoArguments = Callable[[ActivationScope, Type], Any] -FactoryCallableType = Union[ - FactoryCallableNoArguments, - FactoryCallableSingleArgument, - FactoryCallableTwoArguments, -] +FactoryCallableType = ( + FactoryCallableNoArguments + | FactoryCallableSingleArgument + | FactoryCallableTwoArguments +) class FactoryWrapperNoArgs: @@ -885,12 +868,12 @@ def __init__( self, *, strict: bool = False, - scope_cls: Optional[Type[ActivationScope]] = None, + scope_cls: Type[ActivationScope] | None = None, ): - self._map: Dict[Type, Callable] = {} - self._aliases: DefaultDict[str, Set[Type]] = defaultdict(set) - self._exact_aliases: Dict[str, Type] = {} - self._provider: Optional[Services] = None + self._map: dict[Type, Callable] = {} + self._aliases: DefaultDict[str, set[Type]] = defaultdict(set) + self._exact_aliases: dict[str, Type] = {} + self._provider: Services | None = None self._scope_cls = scope_cls self.strict = strict @@ -946,7 +929,7 @@ def register( def resolve( self, - obj_type: Union[Type[T], str], + obj_type: Type[T] | str, scope: Any = None, *args, **kwargs, @@ -1027,7 +1010,7 @@ def _bind(self, key: Type, value: Any) -> None: self._aliases[to_standard_param_name(key_name)].add(key) def add_instance( - self, instance: Any, declared_class: Optional[Type] = None + self, instance: Any, declared_class: Type | None = None ) -> "Container": """ Registers an exact instance, optionally by declared class. @@ -1044,7 +1027,7 @@ def add_instance( return self def add_singleton( - self, base_type: Type, concrete_type: Optional[Type] = None + self, base_type: Type, concrete_type: Type | None = None ) -> "Container": """ Registers a type by base type, to be instantiated with singleton lifetime. @@ -1061,7 +1044,7 @@ def add_singleton( return self.bind_types(base_type, concrete_type, ServiceLifeStyle.SINGLETON) def add_scoped( - self, base_type: Type, concrete_type: Optional[Type] = None + self, base_type: Type, concrete_type: Type | None = None ) -> "Container": """ Registers a type by base type, to be instantiated with scoped lifetime. @@ -1078,7 +1061,7 @@ def add_scoped( return self.bind_types(base_type, concrete_type, ServiceLifeStyle.SCOPED) def add_transient( - self, base_type: Type, concrete_type: Optional[Type] = None + self, base_type: Type, concrete_type: Type | None = None ) -> "Container": """ Registers a type by base type, to be instantiated with transient lifetime. @@ -1136,19 +1119,19 @@ def _add_exact_transient(self, concrete_type: Type) -> "Container": return self def add_singleton_by_factory( - self, factory: FactoryCallableType, return_type: Optional[Type] = None + self, factory: FactoryCallableType, return_type: Type | None = None ) -> "Container": self.register_factory(factory, return_type, ServiceLifeStyle.SINGLETON) return self def add_transient_by_factory( - self, factory: FactoryCallableType, return_type: Optional[Type] = None + self, factory: FactoryCallableType, return_type: Type | None = None ) -> "Container": self.register_factory(factory, return_type, ServiceLifeStyle.TRANSIENT) return self def add_scoped_by_factory( - self, factory: FactoryCallableType, return_type: Optional[Type] = None + self, factory: FactoryCallableType, return_type: Type | None = None ) -> "Container": self.register_factory(factory, return_type, ServiceLifeStyle.SCOPED) return self @@ -1173,7 +1156,7 @@ def _check_factory(factory, signature, handled_type) -> Callable: def register_factory( self, factory: Callable, - return_type: Optional[Type], + return_type: Type | None, life_style: ServiceLifeStyle, ) -> None: if not callable(factory): @@ -1209,7 +1192,7 @@ def build_provider(self) -> Services: :return: Service provider that can be used to activate and obtain services. """ with ResolutionContext() as context: - _map: Dict[Union[str, Type], Type] = {} + _map: dict[str | Type, Type] = {} for _type, resolver in self._map.items(): if isinstance(resolver, DynamicResolver):