From 5194093b189a840d43d3be5547454231717cbb50 Mon Sep 17 00:00:00 2001 From: David Runemalm Date: Mon, 9 Jun 2025 14:26:35 +0200 Subject: [PATCH 01/10] Add support for optionals. --- src/dependency_injection/container.py | 163 +++++++++++------- .../resolve/test_resolve_with_args.py | 72 ++++++++ .../test_resolve_with_default_values.py | 58 +++++++ .../resolve/test_resolve_with_optionals.py | 42 +++++ 4 files changed, 273 insertions(+), 62 deletions(-) create mode 100644 tests/unit_test/container/resolve/test_resolve_with_default_values.py create mode 100644 tests/unit_test/container/resolve/test_resolve_with_optionals.py diff --git a/src/dependency_injection/container.py b/src/dependency_injection/container.py index 1b3dec8..5b7e78c 100644 --- a/src/dependency_injection/container.py +++ b/src/dependency_injection/container.py @@ -3,6 +3,17 @@ from typing import Any, Callable, Dict, List, Optional, TypeVar, Type, Union +try: + from typing import get_origin, get_args +except ImportError: + # Fallback if on Python <= 3.8 + def get_origin(tp): + return getattr(tp, "__origin__", None) + + def get_args(tp): + return getattr(tp, "__args__", ()) + + from dependency_injection.tags.all_tagged import AllTagged from dependency_injection.tags.any_tagged import AnyTagged from dependency_injection.tags.tagged import Tagged @@ -11,6 +22,7 @@ from dependency_injection.utils.singleton_meta import SingletonMeta Self = TypeVar("Self", bound="DependencyContainer") +NoneType = type(None) DEFAULT_CONTAINER_NAME = "default_container" @@ -206,12 +218,23 @@ def _validate_constructor_args( expected_type = constructor[arg_name].annotation if expected_type != inspect.Parameter.empty: - if not isinstance(arg_value, expected_type): - raise TypeError( - f"Constructor argument '{arg_name}' has an incompatible type. " - f"Expected type: {expected_type}, " - f"provided type: {type(arg_value)}." - ) + if self._is_optional_type(expected_type): + real_type = self._unwrap_optional_type(expected_type) + if not isinstance(arg_value, real_type) and arg_value is not None: + raise TypeError( + f"Constructor argument '{arg_name}' " + f"has an incompatible type. " + f"Expected type: {expected_type}, " + f"provided type: {type(arg_value)}." + ) + else: + if not isinstance(arg_value, expected_type): + raise TypeError( + f"Constructor argument '{arg_name}' " + f"has an incompatible type. " + f"Expected type: {expected_type}, " + f"provided type: {type(arg_value)}." + ) def _validate_registration(self, dependency: Type) -> None: if dependency in self._registrations: @@ -228,62 +251,78 @@ def _inject_dependencies( if is_dataclass(implementation): return implementation() # Do not inject into dataclasses - constructor = inspect.signature(implementation.__init__) - params = constructor.parameters + dependencies = self._resolve_constructor_args( + implementation, scope_name, constructor_args + ) + return implementation(**dependencies) + def _resolve_constructor_args( + self, + implementation: Type, + scope_name: str, + constructor_args: Optional[Dict[str, Any]], + ) -> Dict[str, Any]: + constructor = inspect.signature(implementation.__init__) dependencies = {} - for param_name, param_info in params.items(): - if param_name != "self": - if param_info.kind == inspect.Parameter.VAR_POSITIONAL: - pass - elif param_info.kind == inspect.Parameter.VAR_KEYWORD: - pass - else: - if constructor_args and param_name in constructor_args: - dependencies[param_name] = constructor_args[param_name] - else: - if ( - hasattr(param_info.annotation, "__origin__") - and param_info.annotation.__origin__ is list - ): - inner_type = param_info.annotation.__args__[0] - - tagged_dependencies = [] - if isinstance(inner_type, type) and issubclass( - inner_type, Tagged - ): - tagged_type = inner_type.tag - tagged_dependencies = self.resolve_all( - tags={tagged_type} - ) - - elif isinstance(inner_type, type) and issubclass( - inner_type, AnyTagged - ): - tagged_dependencies = self.resolve_all( - tags=inner_type.tags, match_all_tags=False - ) - - elif isinstance(inner_type, type) and issubclass( - inner_type, AllTagged - ): - tagged_dependencies = self.resolve_all( - tags=inner_type.tags, match_all_tags=True - ) - - dependencies[param_name] = tagged_dependencies - - else: - try: - dependencies[param_name] = self.resolve( - param_info.annotation, scope_name=scope_name - ) - except KeyError: - raise ValueError( - f"Cannot resolve dependency for parameter " - f"'{param_name}' of type " - f"'{param_info.annotation}' in class " - f"'{implementation.__name__}'." - ) - return implementation(**dependencies) + for name, param in constructor.parameters.items(): + if name == "self": + continue + if param.kind in ( + inspect.Parameter.VAR_POSITIONAL, + inspect.Parameter.VAR_KEYWORD, + ): + continue + + if constructor_args and name in constructor_args: + dependencies[name] = constructor_args[name] + else: + try: + dependencies[name] = self._resolve_param_value(param, scope_name) + except KeyError: + if self._should_use_default(param): + continue + raise ValueError( + f"Cannot resolve dependency for parameter '{name}' " + f"of type '{param.annotation}' in class " + f"'{implementation.__name__}'." + ) + + return dependencies + + def _resolve_param_value(self, param: inspect.Parameter, scope_name: str) -> Any: + annotation = param.annotation + + if get_origin(annotation) is list: + return self._resolve_list_dependency(annotation) + + if self._is_optional_type(annotation): + inner = self._unwrap_optional_type(annotation) + try: + return self.resolve(inner, scope_name) + except KeyError: + if self._should_use_default(param): + raise KeyError # signal to fallback to default + return None + + return self.resolve(annotation, scope_name) + + def _resolve_list_dependency(self, annotation: Any) -> List[Any]: + inner = get_args(annotation)[0] + if isinstance(inner, type) and issubclass(inner, Tagged): + return self.resolve_all(tags={inner.tag}) + elif isinstance(inner, type) and issubclass(inner, AnyTagged): + return self.resolve_all(tags=inner.tags, match_all_tags=False) + elif isinstance(inner, type) and issubclass(inner, AllTagged): + return self.resolve_all(tags=inner.tags, match_all_tags=True) + else: + raise ValueError(f"Unsupported list injection type: {annotation}") + + def _is_optional_type(self, annotation: Any) -> bool: + return get_origin(annotation) is Union and type(None) in get_args(annotation) + + def _unwrap_optional_type(self, annotation: Any) -> Any: + return next(arg for arg in get_args(annotation) if arg is not NoneType) + + def _should_use_default(self, param_info: inspect.Parameter) -> bool: + return param_info.default is not inspect.Parameter.empty diff --git a/tests/unit_test/container/resolve/test_resolve_with_args.py b/tests/unit_test/container/resolve/test_resolve_with_args.py index 8894cb5..55c8b88 100644 --- a/tests/unit_test/container/resolve/test_resolve_with_args.py +++ b/tests/unit_test/container/resolve/test_resolve_with_args.py @@ -1,3 +1,5 @@ +from typing import Optional + import pytest from dependency_injection.container import DependencyContainer @@ -150,3 +152,73 @@ def create(cls, color: str, engine: Engine) -> Car: self.assertIsInstance(resolved_dependency, Car) self.assertEqual("red", resolved_dependency.color) self.assertIsInstance(resolved_dependency.engine, Engine) + + def test_optional_dependency_overridden_by_constructor_args(self): + # arrange + class Engine: + def __init__(self, name: str): + self.name = name + + class Car: + def __init__(self, engine: Optional[Engine] = None): + self.engine = engine + + engine_instance = Engine("Manual") + + dependency_container = DependencyContainer.get_instance() + dependency_container.register_transient( + Car, constructor_args={"engine": engine_instance} + ) + + # act + resolved_car = dependency_container.resolve(Car) + + # assert + self.assertEqual(resolved_car.engine.name, "Manual") + + def test_optional_dependency_not_registered_but_constructor_arg_provided(self): + # arrange + class Engine: + def __init__(self, name: str): + self.name = name + + class Car: + def __init__(self, engine: Optional[Engine] = None): + self.engine = engine + + dependency_container = DependencyContainer.get_instance() + dependency_container.register_transient( + Car, constructor_args={"engine": Engine("Fallback")} + ) + + # act + resolved_car = dependency_container.resolve(Car) + + # assert + self.assertEqual(resolved_car.engine.name, "Fallback") + + def test_optional_dependency_registered_but_constructor_arg_still_takes_precedence( + self, + ): + # arrange + class Engine: + def __init__(self, name: str): + self.name = name + + class Car: + def __init__(self, engine: Optional[Engine] = None): + self.engine = engine + + dependency_container = DependencyContainer.get_instance() + dependency_container.register_transient( + Engine, constructor_args={"name": "Auto"} + ) + dependency_container.register_transient( + Car, constructor_args={"engine": Engine("Manual")} + ) + + # act + resolved_car = dependency_container.resolve(Car) + + # assert + self.assertEqual(resolved_car.engine.name, "Manual") # not Auto diff --git a/tests/unit_test/container/resolve/test_resolve_with_default_values.py b/tests/unit_test/container/resolve/test_resolve_with_default_values.py new file mode 100644 index 0000000..30e616e --- /dev/null +++ b/tests/unit_test/container/resolve/test_resolve_with_default_values.py @@ -0,0 +1,58 @@ +from typing import Optional +from dependency_injection.container import DependencyContainer +from unit_test.unit_test_case import UnitTestCase + + +class TestResolveWithDefaultValues(UnitTestCase): + def test_resolve_optional_dependency_uses_default_value_when_not_registered(self): + # arrange + class Engine: + pass + + class Car: + def __init__(self, engine: Optional[Engine] = "default_engine"): + self.engine = engine + + dependency_container = DependencyContainer.get_instance() + dependency_container.register_transient(Car) + + # act + resolved_car = dependency_container.resolve(Car) + + # assert + self.assertEqual(resolved_car.engine, "default_engine") + + def test_resolve_uses_default_value_for_non_optional_when_not_registered(self): + # arrange + class Car: + def __init__(self, color: str = "blue"): + self.color = color + + dependency_container = DependencyContainer.get_instance() + dependency_container.register_transient(Car) + + # act + resolved_car = dependency_container.resolve(Car) + + # assert + self.assertEqual(resolved_car.color, "blue") + + def test_resolve_with_mixed_default_and_optional_values(self): + # arrange + class Engine: + pass + + class Car: + def __init__(self, color: str = "blue", engine: Optional[Engine] = None): + self.color = color + self.engine = engine + + dependency_container = DependencyContainer.get_instance() + dependency_container.register_transient(Car) + + # act + resolved_car = dependency_container.resolve(Car) + + # assert + self.assertEqual(resolved_car.color, "blue") + self.assertIsNone(resolved_car.engine) diff --git a/tests/unit_test/container/resolve/test_resolve_with_optionals.py b/tests/unit_test/container/resolve/test_resolve_with_optionals.py new file mode 100644 index 0000000..8a2b5db --- /dev/null +++ b/tests/unit_test/container/resolve/test_resolve_with_optionals.py @@ -0,0 +1,42 @@ +from typing import Optional +from dependency_injection.container import DependencyContainer +from unit_test.unit_test_case import UnitTestCase + + +class TestResolveWithOptionals(UnitTestCase): + def test_resolve_optional_dependency_with_none_when_not_registered(self): + # arrange + class Engine: + pass + + class Car: + def __init__(self, engine: Optional[Engine] = None): + self.engine = engine + + dependency_container = DependencyContainer.get_instance() + dependency_container.register_transient(Car) + + # act + resolved_car = dependency_container.resolve(Car) + + # assert + self.assertIsNone(resolved_car.engine) + + def test_resolve_optional_dependency_when_registered(self): + # arrange + class Engine: + pass + + class Car: + def __init__(self, engine: Optional[Engine] = None): + self.engine = engine + + dependency_container = DependencyContainer.get_instance() + dependency_container.register_transient(Engine) + dependency_container.register_transient(Car) + + # act + resolved_car = dependency_container.resolve(Car) + + # assert + self.assertIsInstance(resolved_car.engine, Engine) From 456a6e61f379cebd79e81f2bcc651d4e164c1b44 Mon Sep 17 00:00:00 2001 From: David Runemalm Date: Mon, 9 Jun 2025 14:29:44 +0200 Subject: [PATCH 02/10] Align flake configuration in makefile with pre-commit configuration. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b159f65..1964f8d 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,7 @@ black-check: ## check code don't violate black formatting rules .PHONY: flake flake: ## lint code with flake - pipenv run flake8 $(SRC) + pipenv run flake8 --max-line-length=88 $(SRC) .PHONY: pre-commit-install pre-commit-install: ## install the pre-commit git hook From 7f5eec7d684e2358fdc9ca71b1e3da89ab869f33 Mon Sep 17 00:00:00 2001 From: David Runemalm Date: Mon, 9 Jun 2025 16:52:41 +0200 Subject: [PATCH 03/10] Bump version to v1.0.0-beta.2 --- README.md | 4 ++++ docs/conf.py | 4 ++-- docs/releases.rst | 6 ++++++ setup.py | 4 ++-- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d88f2ed..3523f8e 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,10 @@ You can find the source code for `py-dependency-injection` on [GitHub](https://g ## Release Notes +### [1.0.0-beta.2](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-beta.2) (2025-06-09) + +- **Enhancement**: Constructor parameters with default values or `Optional[...]` are now supported without requiring explicit registration. + ### [1.0.0-beta.1](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-beta.1) (2025-01-06) - **Transition to Beta**: Transitioned from alpha to beta. Features have been stabilized and are ready for broader testing. diff --git a/docs/conf.py b/docs/conf.py index f44e7ca..d92f95b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,14 +28,14 @@ # -- Project information ----------------------------------------------------- project = "py-dependency-injection" -copyright = "2024, David Runemalm" +copyright = "2025, David Runemalm" author = "David Runemalm" # The version version = "1.0" # The full version, including alpha/beta/rc tags -release = "1.0.0-beta.1" +release = "1.0.0-beta.2" # -- General configuration --------------------------------------------------- diff --git a/docs/releases.rst b/docs/releases.rst index b55df2c..c095d60 100644 --- a/docs/releases.rst +++ b/docs/releases.rst @@ -7,6 +7,12 @@ Version History ############### +**1.0.0-beta.2 (2025-06-09)** + +- **Enhancement**: Constructor parameters with default values or `Optional[...]` are now supported without requiring explicit registration. + +`View release on GitHub `_ + **1.0.0-beta.1 (2025-01-06)** - **Transition to Beta**: Transitioned from alpha to beta. Features have been stabilized and are ready for broader testing. diff --git a/setup.py b/setup.py index aebf618..fdea90a 100644 --- a/setup.py +++ b/setup.py @@ -6,8 +6,8 @@ setup( name="py-dependency-injection", - version="1.0.0-beta.1", - author="David Runemalm, 2024", + version="1.0.0-beta.2", + author="David Runemalm, 2025", author_email="david.runemalm@gmail.com", description="A dependency injection library for Python.", long_description=long_description, From 4cbf67785ff23ddece5c0968c43987f73e20b6d6 Mon Sep 17 00:00:00 2001 From: David Runemalm Date: Mon, 9 Jun 2025 16:58:00 +0200 Subject: [PATCH 04/10] Change ubuntu image in workflows. --- .github/workflows/feature.yml | 4 ++-- .github/workflows/master.yml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/feature.yml b/.github/workflows/feature.yml index c3cdf90..9b25489 100644 --- a/.github/workflows/feature.yml +++ b/.github/workflows/feature.yml @@ -8,7 +8,7 @@ on: jobs: pre-commit: name: Run pre-commit - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout code uses: actions/checkout@v4 @@ -23,7 +23,7 @@ jobs: unittests: name: Run tests - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: matrix: diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index a085e27..afb3bc6 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -9,7 +9,7 @@ on: jobs: pre-commit: name: Run pre-commit - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout code @@ -25,7 +25,7 @@ jobs: unittests: name: Run unittests - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: matrix: @@ -45,7 +45,7 @@ jobs: name: Pre-release (test-pypi) needs: unittests if: success() - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout code @@ -72,7 +72,7 @@ jobs: name: Release (pypi) needs: prerelease if: success() && (github.event_name == 'workflow_dispatch') - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Download artifact From 37c2fdcb2114ec02863721a4f4bd3673b0a00cc5 Mon Sep 17 00:00:00 2001 From: David Runemalm Date: Mon, 9 Jun 2025 17:00:44 +0200 Subject: [PATCH 05/10] Change image to ubuntu latest and container to python 3.7. --- .github/workflows/feature.yml | 6 ++++-- .github/workflows/master.yml | 12 ++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/feature.yml b/.github/workflows/feature.yml index 9b25489..fc8aefa 100644 --- a/.github/workflows/feature.yml +++ b/.github/workflows/feature.yml @@ -8,7 +8,8 @@ on: jobs: pre-commit: name: Run pre-commit - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest + container: python:3.7 steps: - name: Checkout code uses: actions/checkout@v4 @@ -23,7 +24,8 @@ jobs: unittests: name: Run tests - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest + container: python:3.7 strategy: matrix: diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index afb3bc6..85b1e10 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -9,7 +9,8 @@ on: jobs: pre-commit: name: Run pre-commit - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest + container: python:3.7 steps: - name: Checkout code @@ -25,7 +26,8 @@ jobs: unittests: name: Run unittests - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest + container: python:3.7 strategy: matrix: @@ -45,7 +47,8 @@ jobs: name: Pre-release (test-pypi) needs: unittests if: success() - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest + container: python:3.7 steps: - name: Checkout code @@ -72,7 +75,8 @@ jobs: name: Release (pypi) needs: prerelease if: success() && (github.event_name == 'workflow_dispatch') - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest + container: python:3.7 steps: - name: Download artifact From e218829b4b78dc5b76fec6756fc33e42cce34168 Mon Sep 17 00:00:00 2001 From: David Runemalm Date: Mon, 9 Jun 2025 17:03:58 +0200 Subject: [PATCH 06/10] Change pipeline image to ubuntu-22.04. --- .github/workflows/feature.yml | 6 ++---- .github/workflows/master.yml | 12 ++++-------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/feature.yml b/.github/workflows/feature.yml index fc8aefa..09e1d44 100644 --- a/.github/workflows/feature.yml +++ b/.github/workflows/feature.yml @@ -8,8 +8,7 @@ on: jobs: pre-commit: name: Run pre-commit - runs-on: ubuntu-latest - container: python:3.7 + runs-on: ubuntu-22.04 steps: - name: Checkout code uses: actions/checkout@v4 @@ -24,8 +23,7 @@ jobs: unittests: name: Run tests - runs-on: ubuntu-latest - container: python:3.7 + runs-on: ubuntu-22.04 strategy: matrix: diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 85b1e10..ca8ba30 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -9,8 +9,7 @@ on: jobs: pre-commit: name: Run pre-commit - runs-on: ubuntu-latest - container: python:3.7 + runs-on: ubuntu-22.04 steps: - name: Checkout code @@ -26,8 +25,7 @@ jobs: unittests: name: Run unittests - runs-on: ubuntu-latest - container: python:3.7 + runs-on: ubuntu-22.04 strategy: matrix: @@ -47,8 +45,7 @@ jobs: name: Pre-release (test-pypi) needs: unittests if: success() - runs-on: ubuntu-latest - container: python:3.7 + runs-on: ubuntu-22.04 steps: - name: Checkout code @@ -75,8 +72,7 @@ jobs: name: Release (pypi) needs: prerelease if: success() && (github.event_name == 'workflow_dispatch') - runs-on: ubuntu-latest - container: python:3.7 + runs-on: ubuntu-22.04 steps: - name: Download artifact From d75376155d08c9395d3243cad4c59eefbed115d2 Mon Sep 17 00:00:00 2001 From: David Runemalm Date: Mon, 9 Jun 2025 17:09:29 +0200 Subject: [PATCH 07/10] Add support for python 3.13. --- .github/workflows/feature.yml | 2 +- .github/workflows/master.yml | 2 +- README.md | 3 ++- docs/releases.rst | 1 + docs/userguide.rst | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/feature.yml b/.github/workflows/feature.yml index 09e1d44..941627d 100644 --- a/.github/workflows/feature.yml +++ b/.github/workflows/feature.yml @@ -27,7 +27,7 @@ jobs: strategy: matrix: - python-version: [3.7.16, 3.8.18, 3.9.18, "3.10.13", 3.11.5, 3.12.0] + python-version: [3.7.16, 3.8.18, 3.9.18, "3.10.13", 3.11.5, 3.12.0, 3.13.3] fail-fast: false steps: diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index ca8ba30..abda78e 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -29,7 +29,7 @@ jobs: strategy: matrix: - python-version: [3.7.16, 3.8.18, 3.9.18, "3.10.13", 3.11.5, 3.12.0] + python-version: [3.7.16, 3.8.18, 3.9.18, "3.10.13", 3.11.5, 3.12.0, 3.13.3] fail-fast: false steps: diff --git a/README.md b/README.md index 3523f8e..606810b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ A dependency injection library for Python. The library is compatible with the following Python versions: -- 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 +- 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 ## Installation @@ -86,6 +86,7 @@ You can find the source code for `py-dependency-injection` on [GitHub](https://g ### [1.0.0-beta.2](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-beta.2) (2025-06-09) - **Enhancement**: Constructor parameters with default values or `Optional[...]` are now supported without requiring explicit registration. +- **Python Version Support**: Added support for Python version 3.13. ### [1.0.0-beta.1](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-beta.1) (2025-01-06) diff --git a/docs/releases.rst b/docs/releases.rst index c095d60..3cd8f93 100644 --- a/docs/releases.rst +++ b/docs/releases.rst @@ -10,6 +10,7 @@ Version History **1.0.0-beta.2 (2025-06-09)** - **Enhancement**: Constructor parameters with default values or `Optional[...]` are now supported without requiring explicit registration. +- **Python Version Support**: Added support for Python version 3.13. `View release on GitHub `_ diff --git a/docs/userguide.rst b/docs/userguide.rst index cae5c35..9be004e 100644 --- a/docs/userguide.rst +++ b/docs/userguide.rst @@ -23,7 +23,7 @@ Install the library using pip: $ pip install py-dependency-injection -The library supports Python versions 3.7 through 3.12. +The library supports Python versions 3.7 through 3.13. ########################## Core Concepts and Features From de7a2ce4838d66517cb0596ae1e894f3d136a36f Mon Sep 17 00:00:00 2001 From: David Runemalm Date: Mon, 9 Jun 2025 17:20:11 +0200 Subject: [PATCH 08/10] Change workflow python version to 3.13.4. --- .github/workflows/feature.yml | 2 +- .github/workflows/master.yml | 2 +- setup.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/feature.yml b/.github/workflows/feature.yml index 941627d..248d355 100644 --- a/.github/workflows/feature.yml +++ b/.github/workflows/feature.yml @@ -27,7 +27,7 @@ jobs: strategy: matrix: - python-version: [3.7.16, 3.8.18, 3.9.18, "3.10.13", 3.11.5, 3.12.0, 3.13.3] + python-version: [3.7.16, 3.8.18, 3.9.18, "3.10.13", 3.11.5, 3.12.0, 3.13.4] fail-fast: false steps: diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index abda78e..66fc3f3 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -29,7 +29,7 @@ jobs: strategy: matrix: - python-version: [3.7.16, 3.8.18, 3.9.18, "3.10.13", 3.11.5, 3.12.0, 3.13.3] + python-version: [3.7.16, 3.8.18, 3.9.18, "3.10.13", 3.11.5, 3.12.0, 3.13.4] fail-fast: false steps: diff --git a/setup.py b/setup.py index fdea90a..7dda779 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries :: Python Modules", ], ) From 7f86caf650969bd2e76325585d40ffacf466b727 Mon Sep 17 00:00:00 2001 From: David Runemalm Date: Mon, 9 Jun 2025 17:33:46 +0200 Subject: [PATCH 09/10] Use other pyenv in workflow. --- .github/actions/unittests/action.yml | 33 ++++++++++++++++++---------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/.github/actions/unittests/action.yml b/.github/actions/unittests/action.yml index 1aec8dc..41cb704 100644 --- a/.github/actions/unittests/action.yml +++ b/.github/actions/unittests/action.yml @@ -11,19 +11,30 @@ runs: - name: Checkout code uses: actions/checkout@v4 - - name: Install .python-version and ${{ inputs.python-version }} - uses: "gabrielfalcao/pyenv-action@v18" - with: - default: 3.7.17 - versions: ${{ inputs.python-version }} + - name: Install pyenv from source + run: | + git clone https://github.com/pyenv/pyenv.git ~/.pyenv + echo "PYENV_ROOT=$HOME/.pyenv" >> $GITHUB_ENV + echo "$HOME/.pyenv/bin" >> $GITHUB_PATH + shell: bash - - name: Switch to .python-version + - name: Install Python ${{ inputs.python-version }} via pyenv run: | - pyenv local "$(cat .python-version)" - if python -V | grep -q "$(cat .python-version)"; then - echo "Python version is '$(python -V)'" - else - echo "Python version is '$(python -V)', but should be '$(cat .python-version)'. Exiting workflow." + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init --path)" + pyenv install -s ${{ inputs.python-version }} + pyenv global ${{ inputs.python-version }} + echo "${{ inputs.python-version }}" > .python-version + shell: bash + + - name: Verify installed Python version + run: | + ACTUAL=$(python -V) + EXPECTED=${{ inputs.python-version }} + echo "Python version installed: $ACTUAL" + if ! python -V | grep -q "$EXPECTED"; then + echo "Python version is not correct. Exiting." exit 1 fi shell: bash From 3dd82d7c1e40f44e2384f1d49e8d13a3792412f6 Mon Sep 17 00:00:00 2001 From: David Runemalm Date: Mon, 9 Jun 2025 17:37:25 +0200 Subject: [PATCH 10/10] Fix pyenv in workflow. --- .github/actions/unittests/action.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/actions/unittests/action.yml b/.github/actions/unittests/action.yml index 41cb704..8fcb170 100644 --- a/.github/actions/unittests/action.yml +++ b/.github/actions/unittests/action.yml @@ -30,6 +30,9 @@ runs: - name: Verify installed Python version run: | + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init --path)" ACTUAL=$(python -V) EXPECTED=${{ inputs.python-version }} echo "Python version installed: $ACTUAL" @@ -41,17 +44,26 @@ runs: - name: Install pipenv run: | + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init --path)" python -m pip install --upgrade pip pip install pipenv shell: bash - name: Setup virtual environment run: | + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init --path)" pipenv --python ${{ inputs.python-version }} install --dev --deploy shell: bash - name: Verify virtual environment uses python version ${{ inputs.python-version }} run: | + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init --path)" if pipenv run python -V | grep -q "${{ inputs.python-version }}"; then echo "Python ${{ inputs.python-version }} is being used." else @@ -62,5 +74,8 @@ runs: - name: Run unittests run: | + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init --path)" PYTHONPATH=./src:./tests pipenv run pytest ./tests shell: bash