modern-di-pytest 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ # Generic things
2
+ *.pyc
3
+ *~
4
+ __pycache__/*
5
+ *.swp
6
+ *.sqlite3
7
+ *.map
8
+ .vscode
9
+ .idea
10
+ .DS_Store
11
+ .env
12
+ .pytest_cache
13
+ .ruff_cache
14
+ .coverage
15
+ htmlcov/
16
+ coverage.xml
17
+ pytest.xml
18
+ dist/
19
+ .python-version
20
+ .venv
21
+ uv.lock
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Artur Shiriev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: modern-di-pytest
3
+ Version: 0.1.0
4
+ Summary: Pytest integration for Modern-DI: turn DI dependencies into pytest fixtures
5
+ Project-URL: repository, https://github.com/modern-python/modern-di-pytest
6
+ Project-URL: docs, https://modern-di.readthedocs.io
7
+ Author-email: Artur Shiriev <me@shiriev.ru>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: DI,dependency injector,fixtures,ioc-container,pytest,python
11
+ Classifier: Framework :: Pytest
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Topic :: Software Development :: Testing
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: <4,>=3.10
21
+ Requires-Dist: modern-di<3,>=2
22
+ Requires-Dist: pytest>=7
23
+ Description-Content-Type: text/markdown
24
+
25
+ "Modern-DI-Pytest"
26
+ ==
27
+ [![Supported versions](https://img.shields.io/pypi/pyversions/modern-di-pytest.svg)](https://pypi.python.org/pypi/modern-di-pytest)
28
+ [![downloads](https://img.shields.io/pypi/dm/modern-di-pytest.svg)](https://pypistats.org/packages/modern-di-pytest)
29
+ [![GitHub stars](https://img.shields.io/github/stars/modern-python/modern-di-pytest)](https://github.com/modern-python/modern-di-pytest/stargazers)
30
+
31
+ Pytest integration for [Modern-DI](https://github.com/modern-python/modern-di).
32
+
33
+ Turn any DI dependency into a pytest fixture with one line.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ uv add --dev modern-di-pytest
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ The user owns the root container fixture. Pick whatever pytest scope fits the test suite:
44
+
45
+ ```python
46
+ # conftest.py
47
+ import typing
48
+
49
+ import modern_di
50
+ import pytest
51
+ from modern_di_pytest import expose, modern_di_fixture
52
+
53
+ from app import ioc
54
+ from app.ioc import Dependencies
55
+ from app.services import EmailClient
56
+
57
+
58
+ @pytest.fixture
59
+ def di_container() -> typing.Iterator[modern_di.Container]:
60
+ with modern_di.Container(groups=ioc.ALL_GROUPS) as container:
61
+ yield container
62
+
63
+
64
+ # Bulk: every Provider on Dependencies becomes a pytest fixture
65
+ # named after the class attribute.
66
+ expose(Dependencies)
67
+
68
+ # Manual: turn a single type or Provider into a named fixture.
69
+ email_client = modern_di_fixture(EmailClient)
70
+ ```
71
+
72
+ Tests then receive resolved dependencies by name:
73
+
74
+ ```python
75
+ from app.services import EmailClient, UserService
76
+
77
+
78
+ def test_listing(user_service: UserService) -> None: # generated by expose(Dependencies)
79
+ assert user_service.list_users() == []
80
+
81
+
82
+ def test_email(email_client: EmailClient) -> None: # generated manually
83
+ email_client.send("hi")
84
+ ```
85
+
86
+ ## Pointing a fixture at a child container
87
+
88
+ ```python
89
+ import typing
90
+
91
+ import modern_di
92
+ import pytest
93
+ from modern_di_pytest import modern_di_fixture
94
+
95
+ from app.services import UserService
96
+
97
+
98
+ @pytest.fixture
99
+ def request_container(
100
+ di_container: modern_di.Container,
101
+ ) -> typing.Iterator[modern_di.Container]:
102
+ with di_container.build_child_container(scope=modern_di.Scope.REQUEST) as container:
103
+ yield container
104
+
105
+
106
+ request_user_service = modern_di_fixture(
107
+ UserService, container_fixture="request_container"
108
+ )
109
+ ```
110
+
111
+ ## Overrides
112
+
113
+ Use `Container.override()` directly — `modern-di` already ships a first-class
114
+ override mechanism backed by a tree-shared `OverridesRegistry`:
115
+
116
+ ```python
117
+ import modern_di
118
+
119
+ from app.ioc import Dependencies
120
+ from app.services import UserService
121
+ from tests.fakes import FakeRepo
122
+
123
+
124
+ def test_with_override(
125
+ di_container: modern_di.Container,
126
+ user_service: UserService,
127
+ ) -> None:
128
+ di_container.override(Dependencies.user_repo, FakeRepo())
129
+ try:
130
+ assert user_service.list_users() == []
131
+ finally:
132
+ di_container.reset_override(Dependencies.user_repo)
133
+ ```
134
+
135
+ ## API
136
+
137
+ ### `modern_di_fixture(dependency, *, container_fixture="di_container", name=None, pytest_scope="function")`
138
+
139
+ Turn a single dependency into a pytest fixture. ``dependency`` is either a
140
+ type (resolved via ``container.resolve``) or a Provider (resolved via
141
+ ``container.resolve_provider``). The returned object is a real pytest fixture
142
+ — assign it to a module-level name and pytest will collect it.
143
+
144
+ ### `expose(group, *, container_fixture="di_container", pytest_scope="function", module=None)`
145
+
146
+ Walk ``group`` (a ``Group`` subclass) and inject one pytest fixture per
147
+ Provider class attribute into the caller's module. Fixture names equal the
148
+ class-attribute names. Non-Provider class attributes are skipped. Pass
149
+ ``module=`` explicitly when stack introspection cannot identify the caller.
150
+
151
+ ## 📚 [Documentation](https://modern-di.readthedocs.io)
@@ -0,0 +1,127 @@
1
+ "Modern-DI-Pytest"
2
+ ==
3
+ [![Supported versions](https://img.shields.io/pypi/pyversions/modern-di-pytest.svg)](https://pypi.python.org/pypi/modern-di-pytest)
4
+ [![downloads](https://img.shields.io/pypi/dm/modern-di-pytest.svg)](https://pypistats.org/packages/modern-di-pytest)
5
+ [![GitHub stars](https://img.shields.io/github/stars/modern-python/modern-di-pytest)](https://github.com/modern-python/modern-di-pytest/stargazers)
6
+
7
+ Pytest integration for [Modern-DI](https://github.com/modern-python/modern-di).
8
+
9
+ Turn any DI dependency into a pytest fixture with one line.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ uv add --dev modern-di-pytest
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ The user owns the root container fixture. Pick whatever pytest scope fits the test suite:
20
+
21
+ ```python
22
+ # conftest.py
23
+ import typing
24
+
25
+ import modern_di
26
+ import pytest
27
+ from modern_di_pytest import expose, modern_di_fixture
28
+
29
+ from app import ioc
30
+ from app.ioc import Dependencies
31
+ from app.services import EmailClient
32
+
33
+
34
+ @pytest.fixture
35
+ def di_container() -> typing.Iterator[modern_di.Container]:
36
+ with modern_di.Container(groups=ioc.ALL_GROUPS) as container:
37
+ yield container
38
+
39
+
40
+ # Bulk: every Provider on Dependencies becomes a pytest fixture
41
+ # named after the class attribute.
42
+ expose(Dependencies)
43
+
44
+ # Manual: turn a single type or Provider into a named fixture.
45
+ email_client = modern_di_fixture(EmailClient)
46
+ ```
47
+
48
+ Tests then receive resolved dependencies by name:
49
+
50
+ ```python
51
+ from app.services import EmailClient, UserService
52
+
53
+
54
+ def test_listing(user_service: UserService) -> None: # generated by expose(Dependencies)
55
+ assert user_service.list_users() == []
56
+
57
+
58
+ def test_email(email_client: EmailClient) -> None: # generated manually
59
+ email_client.send("hi")
60
+ ```
61
+
62
+ ## Pointing a fixture at a child container
63
+
64
+ ```python
65
+ import typing
66
+
67
+ import modern_di
68
+ import pytest
69
+ from modern_di_pytest import modern_di_fixture
70
+
71
+ from app.services import UserService
72
+
73
+
74
+ @pytest.fixture
75
+ def request_container(
76
+ di_container: modern_di.Container,
77
+ ) -> typing.Iterator[modern_di.Container]:
78
+ with di_container.build_child_container(scope=modern_di.Scope.REQUEST) as container:
79
+ yield container
80
+
81
+
82
+ request_user_service = modern_di_fixture(
83
+ UserService, container_fixture="request_container"
84
+ )
85
+ ```
86
+
87
+ ## Overrides
88
+
89
+ Use `Container.override()` directly — `modern-di` already ships a first-class
90
+ override mechanism backed by a tree-shared `OverridesRegistry`:
91
+
92
+ ```python
93
+ import modern_di
94
+
95
+ from app.ioc import Dependencies
96
+ from app.services import UserService
97
+ from tests.fakes import FakeRepo
98
+
99
+
100
+ def test_with_override(
101
+ di_container: modern_di.Container,
102
+ user_service: UserService,
103
+ ) -> None:
104
+ di_container.override(Dependencies.user_repo, FakeRepo())
105
+ try:
106
+ assert user_service.list_users() == []
107
+ finally:
108
+ di_container.reset_override(Dependencies.user_repo)
109
+ ```
110
+
111
+ ## API
112
+
113
+ ### `modern_di_fixture(dependency, *, container_fixture="di_container", name=None, pytest_scope="function")`
114
+
115
+ Turn a single dependency into a pytest fixture. ``dependency`` is either a
116
+ type (resolved via ``container.resolve``) or a Provider (resolved via
117
+ ``container.resolve_provider``). The returned object is a real pytest fixture
118
+ — assign it to a module-level name and pytest will collect it.
119
+
120
+ ### `expose(group, *, container_fixture="di_container", pytest_scope="function", module=None)`
121
+
122
+ Walk ``group`` (a ``Group`` subclass) and inject one pytest fixture per
123
+ Provider class attribute into the caller's module. Fixture names equal the
124
+ class-attribute names. Non-Provider class attributes are skipped. Pass
125
+ ``module=`` explicitly when stack introspection cannot identify the caller.
126
+
127
+ ## 📚 [Documentation](https://modern-di.readthedocs.io)
@@ -0,0 +1,7 @@
1
+ from modern_di_pytest.factory import expose, modern_di_fixture
2
+
3
+
4
+ __all__ = [
5
+ "expose",
6
+ "modern_di_fixture",
7
+ ]
@@ -0,0 +1,103 @@
1
+ import inspect
2
+ import types
3
+ import typing
4
+
5
+ import pytest
6
+ from modern_di.group import Group
7
+ from modern_di.providers.abstract import AbstractProvider
8
+
9
+
10
+ T = typing.TypeVar("T")
11
+
12
+ _PytestScope = typing.Literal["function", "class", "module", "package", "session"]
13
+
14
+
15
+ def modern_di_fixture(
16
+ dependency: type[T] | AbstractProvider[T],
17
+ *,
18
+ container_fixture: str = "di_container",
19
+ name: str | None = None,
20
+ pytest_scope: _PytestScope = "function",
21
+ ) -> typing.Any: # noqa: ANN401
22
+ """Turn a modern-di dependency into a pytest fixture.
23
+
24
+ Args:
25
+ dependency: A type (resolved via ``container.resolve``) or a Provider
26
+ (resolved via ``container.resolve_provider``).
27
+ container_fixture: Name of the pytest fixture that yields the container
28
+ to resolve from. Defaults to ``"di_container"``. Use a child
29
+ container fixture (e.g. ``"di_request_container"``) to resolve at a
30
+ deeper scope.
31
+ name: Optional pytest fixture name. Defaults to the assigned variable
32
+ name in the conftest.
33
+ pytest_scope: pytest fixture scope (``"function"`` by default).
34
+
35
+ Returns:
36
+ A pytest fixture object. Assign it to a module-level name and pytest
37
+ will collect it.
38
+
39
+ Example::
40
+
41
+ user_service = modern_di_fixture(UserService)
42
+
43
+
44
+ def test_listing(user_service):
45
+ assert user_service.list_users() == []
46
+
47
+ """
48
+
49
+ @pytest.fixture(name=name, scope=pytest_scope)
50
+ def _fixture(request: pytest.FixtureRequest) -> typing.Any: # noqa: ANN401
51
+ container = request.getfixturevalue(container_fixture)
52
+ if isinstance(dependency, AbstractProvider):
53
+ return container.resolve_provider(dependency)
54
+ return container.resolve(dependency)
55
+
56
+ return _fixture
57
+
58
+
59
+ def expose(
60
+ group: type[Group],
61
+ *,
62
+ container_fixture: str = "di_container",
63
+ pytest_scope: _PytestScope = "function",
64
+ module: types.ModuleType | None = None,
65
+ ) -> None:
66
+ """Register one pytest fixture per Provider in ``group``.
67
+
68
+ Each generated fixture is named after the class attribute it came from.
69
+
70
+ Args:
71
+ group: A ``Group`` subclass whose class attributes are Providers.
72
+ container_fixture: Name of the pytest fixture yielding the container.
73
+ pytest_scope: pytest fixture scope applied to every generated fixture.
74
+ module: Module to inject fixtures into. Defaults to the caller's module
75
+ (located via ``inspect.stack()``).
76
+
77
+ Example (in ``conftest.py``)::
78
+
79
+ from modern_di_pytest import expose
80
+ from app.ioc import Dependencies
81
+
82
+ expose(Dependencies)
83
+
84
+ Every ``Dependencies.<attr>`` that is a Provider becomes a pytest fixture
85
+ named ``<attr>``. Non-Provider class attributes are skipped.
86
+
87
+ """
88
+ if module is None:
89
+ frame = inspect.stack()[1].frame
90
+ module = inspect.getmodule(frame)
91
+ if module is None:
92
+ msg = "expose() could not determine the caller module; pass module=... explicitly."
93
+ raise RuntimeError(msg)
94
+
95
+ for attr_name, attr_value in vars(group).items():
96
+ if isinstance(attr_value, AbstractProvider):
97
+ fixture = modern_di_fixture(
98
+ attr_value,
99
+ container_fixture=container_fixture,
100
+ name=attr_name,
101
+ pytest_scope=pytest_scope,
102
+ )
103
+ setattr(module, attr_name, fixture)
File without changes
@@ -0,0 +1,77 @@
1
+ [project]
2
+ name = "modern-di-pytest"
3
+ description = "Pytest integration for Modern-DI: turn DI dependencies into pytest fixtures"
4
+ authors = [{ name = "Artur Shiriev", email = "me@shiriev.ru" }]
5
+ requires-python = ">=3.10,<4"
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ keywords = ["DI", "dependency injector", "ioc-container", "pytest", "fixtures", "python"]
9
+ classifiers = [
10
+ "Framework :: Pytest",
11
+ "Programming Language :: Python :: 3.10",
12
+ "Programming Language :: Python :: 3.11",
13
+ "Programming Language :: Python :: 3.12",
14
+ "Programming Language :: Python :: 3.13",
15
+ "Programming Language :: Python :: 3.14",
16
+ "Typing :: Typed",
17
+ "Topic :: Software Development :: Libraries",
18
+ "Topic :: Software Development :: Testing",
19
+ ]
20
+ dependencies = ["modern-di>=2,<3", "pytest>=7"]
21
+ version = "0.1.0"
22
+
23
+ [project.urls]
24
+ repository = "https://github.com/modern-python/modern-di-pytest"
25
+ docs = "https://modern-di.readthedocs.io"
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "pytest",
30
+ "pytest-cov",
31
+ ]
32
+ lint = [
33
+ "ty",
34
+ "ruff",
35
+ "eof-fixer",
36
+ "typing-extensions",
37
+ ]
38
+
39
+ [build-system]
40
+ requires = ["hatchling"]
41
+ build-backend = "hatchling.build"
42
+
43
+ [tool.hatch.build]
44
+ include = ["modern_di_pytest"]
45
+
46
+ [tool.ruff]
47
+ fix = false
48
+ unsafe-fixes = true
49
+ line-length = 120
50
+ target-version = "py310"
51
+
52
+ [tool.ruff.format]
53
+ docstring-code-format = true
54
+
55
+ [tool.ruff.lint]
56
+ select = ["ALL"]
57
+ ignore = [
58
+ "D1",
59
+ "S101",
60
+ "TCH",
61
+ "FBT",
62
+ "D203",
63
+ "D213",
64
+ "COM812",
65
+ "ISC001",
66
+ "G004",
67
+ ]
68
+ isort.lines-after-imports = 2
69
+ isort.no-lines-before = ["standard-library", "local-folder"]
70
+
71
+ [tool.pytest.ini_options]
72
+ addopts = "--cov=. --cov-report term-missing"
73
+
74
+ [tool.coverage.report]
75
+ exclude_also = [
76
+ "if typing.TYPE_CHECKING:",
77
+ ]