python-di-application 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,50 @@
1
+ name: Publish Python Package
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ release-build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.12"
19
+
20
+ - name: Build release distributions
21
+ run: |
22
+ python -m pip install --upgrade build twine
23
+ python -m build
24
+ python -m twine check dist/*
25
+
26
+ - name: Upload distributions
27
+ uses: actions/upload-artifact@v4
28
+ with:
29
+ name: release-dists
30
+ path: dist/
31
+
32
+ pypi-publish:
33
+ runs-on: ubuntu-latest
34
+ needs: [release-build]
35
+ permissions:
36
+ id-token: write
37
+ environment:
38
+ name: pypi
39
+ url: https://pypi.org/project/python-di/
40
+ steps:
41
+ - name: Retrieve release distributions
42
+ uses: actions/download-artifact@v4
43
+ with:
44
+ name: release-dists
45
+ path: dist/
46
+
47
+ - name: Publish release distributions to PyPI
48
+ uses: pypa/gh-action-pypi-publish@release/v1
49
+ with:
50
+ packages-dir: dist/
@@ -0,0 +1,146 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ warehouse/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ test_report
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+ reports/
55
+
56
+ # Translations
57
+ *.mo
58
+ *.pot
59
+
60
+ # Django stuff:
61
+ *.log
62
+ local_settings.py
63
+ db.sqlite3
64
+ db.sqlite3-journal
65
+
66
+ # Flask stuff:
67
+ instance/
68
+ .webassets-cache
69
+
70
+ # Scrapy stuff:
71
+ .scrapy
72
+
73
+ # Sphinx documentation
74
+ docs/_build/
75
+
76
+ # PyBuilder
77
+ .pybuilder/
78
+ target/
79
+
80
+ # Jupyter Notebook
81
+ .ipynb_checkpoints
82
+
83
+ # IPython
84
+ profile_default/
85
+ ipython_config.py
86
+
87
+ # pyenv
88
+ # For a library or package, you might want to ignore these files since the code is
89
+ # intended to run in multiple environments; otherwise, check them in:
90
+ # .python-version
91
+
92
+ # pipenv
93
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
95
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
96
+ # install all needed dependencies.
97
+ #Pipfile.lock
98
+
99
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
100
+ __pypackages__/
101
+
102
+ # Celery stuff
103
+ celerybeat-schedule
104
+ celerybeat.pid
105
+
106
+ # SageMath parsed files
107
+ *.sage.py
108
+
109
+ # Environments
110
+ .env
111
+ .venv
112
+ env/
113
+ venv/
114
+ ENV/
115
+ env.bak/
116
+ venv.bak/
117
+
118
+ # Spyder project settings
119
+ .spyderproject
120
+ .spyproject
121
+
122
+ # Rope project settings
123
+ .ropeproject
124
+
125
+ # mkdocs documentation
126
+ /site
127
+
128
+ # mypy
129
+ .mypy_cache/
130
+ .dmypy.json
131
+ dmypy.json
132
+
133
+ # Pyre type checker
134
+ .pyre/
135
+
136
+ # pytype static type analyzer
137
+ .pytype/
138
+
139
+ # Cython debug symbols
140
+ cython_debug/
141
+ /tools/docker/warehouse/
142
+ .openapi-generator/
143
+ .ivy2/
144
+ .uv-cache/
145
+ .tools/
146
+ .idea/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 alex-timmermann
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,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-di-application
3
+ Version: 0.1.0
4
+ Summary: Lightweight dependency injection container for Python with automatic constructor wiring and configuration overrides.
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: pydantic-settings>=2.13.1
8
+ Requires-Dist: pydantic>=2.12.5
@@ -0,0 +1,96 @@
1
+ # Python-DI
2
+ Python-DI is a lightweight dependency injection container for Python applications.
3
+ It automatically resolves constructor dependencies, supports singleton-style
4
+ instance reuse, and allows runtime overrides for both dependencies and
5
+ configuration objects (for example `pydantic` settings), as demonstrated in the
6
+ `src/python_di/example` application.
7
+
8
+ ## Entry point example
9
+
10
+ ```python
11
+ from python_di.di_container import DependencyInstance
12
+ from python_di.example.example_app import ExampleApp
13
+ from python_di.example.services.config_b import ConfigB
14
+
15
+
16
+ def main():
17
+ app = ExampleApp.build()
18
+ app.run()
19
+
20
+ # or override the dependencies
21
+ app_2 = ExampleApp.build(
22
+ override_instances=[
23
+ DependencyInstance(instance_obj=ConfigB(config_b="override")),
24
+ ]
25
+ )
26
+ app_2.run()
27
+
28
+
29
+ if __name__ == "__main__":
30
+ main()
31
+ ```
32
+
33
+ ## Example app
34
+
35
+ ```python
36
+ from python_di.application import Application
37
+ from python_di.di_container import DIContainer, Dependency
38
+ from python_di.example.services.config_b import ConfigB
39
+ from python_di.example.services.service_a import ServiceA
40
+ from python_di.example.services.service_b import ServiceB
41
+
42
+
43
+ class ExampleApp(Application):
44
+ def __init__(self, service_a: ServiceA) -> None:
45
+ self._service_a = service_a
46
+
47
+ @classmethod
48
+ def _default_container(cls) -> DIContainer:
49
+ di = DIContainer()
50
+ di.register_dependencies(
51
+ dependencies_types_with_kwargs=[
52
+ Dependency(ServiceA),
53
+ Dependency(ServiceB),
54
+ Dependency(ConfigB),
55
+ Dependency(cls),
56
+ ]
57
+ )
58
+ return di
59
+
60
+ @classmethod
61
+ def _build(cls, container: DIContainer) -> tuple[DIContainer, "ExampleApp"]:
62
+ return container, container.resolve_dependency(dependency_type=cls)
63
+
64
+ def run(self):
65
+ self._service_a.do_random()
66
+ ```
67
+
68
+ ## How the setup works
69
+
70
+ Start with the `Entry point example`: `main()` calls `ExampleApp.build()` and then
71
+ `app.run()`.
72
+
73
+ In the `Example app`, `_default_container()` defines the dependency graph by
74
+ registering `ServiceA`, `ServiceB`, `ConfigB`, and `ExampleApp` itself. During
75
+ `build()`, the container resolves constructor dependencies from type hints:
76
+
77
+ - `ExampleApp` needs `ServiceA`
78
+ - `ServiceA` needs `ServiceB`
79
+ - `ServiceB` needs `ConfigB`
80
+
81
+ That means you only declare dependencies in constructors, and the container wires
82
+ the full object graph automatically.
83
+
84
+ ## Why this is useful
85
+
86
+ The two examples together show the main advantages of `python-di`:
87
+
88
+ - Centralized wiring: dependency registration is in one place (`_default_container()`),
89
+ instead of scattered factory code.
90
+ - Cleaner services: service classes only declare what they need in `__init__`,
91
+ without manually instantiating collaborators.
92
+ - Easy runtime overrides: the `Entry point example` shows
93
+ `override_instances=[DependencyInstance(...)]`, which lets you swap config or
94
+ test doubles without changing application code.
95
+ - Better testability: because dependencies are injected, unit tests can replace
96
+ concrete objects with lightweight alternatives.
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "python-di-application"
3
+ version = "0.1.0"
4
+ description = "Lightweight dependency injection container for Python with automatic constructor wiring and configuration overrides."
5
+ requires-python = ">=3.12"
6
+ dependencies = [
7
+ "pydantic>=2.12.5",
8
+ "pydantic-settings>=2.13.1",
9
+ ]
10
+
11
+ [dependency-groups]
12
+ dev = [
13
+ "pydantic>=2.12.5",
14
+ "ruff>=0.15.4",
15
+ ]
16
+
17
+
18
+ [tool.uv]
19
+ package = true
20
+
21
+ [build-system]
22
+ requires = ["hatchling>=1.25.0"]
23
+ build-backend = "hatchling.build"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/python_di"]
@@ -0,0 +1,70 @@
1
+ import logging
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any
4
+
5
+ from python_di_application.di_container import Dependency, DependencyInstance, DIContainer
6
+
7
+
8
+ class Application(ABC):
9
+ @classmethod
10
+ def default_container(
11
+ cls,
12
+ override_dependencies: list[Dependency[Any]] | None = None,
13
+ override_instances: list[DependencyInstance] | None = None,
14
+ ) -> DIContainer:
15
+ container = cls._default_container()
16
+ if override_dependencies:
17
+ container.override_dependencies(
18
+ dependencies_types_with_kwargs=override_dependencies
19
+ )
20
+ if override_instances:
21
+ container.replace_dependency_instances(
22
+ dependency_instances=override_instances
23
+ )
24
+ return container
25
+
26
+ @classmethod
27
+ @abstractmethod
28
+ def _default_container(cls) -> DIContainer:
29
+ pass
30
+
31
+ @classmethod
32
+ def build[T](
33
+ cls: type[T],
34
+ container: DIContainer | None = None,
35
+ override_dependencies: list[Dependency] | None = None,
36
+ override_instances: list[DependencyInstance] | None = None,
37
+ ignore_unused_dependencies: bool = False,
38
+ ) -> T:
39
+ if container is None:
40
+ container = cls.default_container( # type: ignore[attr-defined]
41
+ override_dependencies=override_dependencies,
42
+ override_instances=override_instances,
43
+ )
44
+
45
+ container, application = cls._build(container=container) # type: ignore[attr-defined]
46
+
47
+ application._attach_container(container=container)
48
+
49
+ if not isinstance(container, DIContainer):
50
+ raise TypeError(
51
+ f"Container must be an instance of DIContainer, but got {type(container)}"
52
+ )
53
+ container.apply_post_init_wrappers()
54
+ if not ignore_unused_dependencies:
55
+ container.check_if_all_dependencies_are_used()
56
+
57
+ logger = logging.getLogger("py4j")
58
+ logger.setLevel(logging.WARNING)
59
+ return application
60
+
61
+ @classmethod
62
+ @abstractmethod
63
+ def _build[T](cls: type[T], container: DIContainer) -> tuple[DIContainer, T]:
64
+ pass
65
+
66
+ def _attach_container(self, container: DIContainer) -> None:
67
+ self._container = container
68
+
69
+ def __getitem__[T](self, item: type[T]) -> T:
70
+ return self._container[item]