aspyx 1.4.1__tar.gz → 1.5.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.
Potentially problematic release.
This version of aspyx might be problematic. Click here for more details.
- aspyx-1.5.0/.gitignore +194 -0
- aspyx-1.5.0/PKG-INFO +33 -0
- aspyx-1.5.0/README.md +1 -0
- aspyx-1.5.0/pyproject.toml +22 -0
- aspyx-1.5.0/src/aspyx/__init__.py +1 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/__init__.py +2 -2
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/di.py +91 -24
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/exception/exception_manager.py +1 -1
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/reflection/proxy.py +33 -6
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/reflection/reflection.py +31 -7
- aspyx-1.5.0/tests/config.yaml +4 -0
- aspyx-1.5.0/tests/config1.yaml +4 -0
- aspyx-1.5.0/tests/di_import.py +14 -0
- aspyx-1.5.0/tests/sub_import.py +14 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/tests/test_configuration.py +2 -2
- {aspyx-1.4.1 → aspyx-1.5.0}/tests/test_cycle.py +11 -1
- {aspyx-1.4.1 → aspyx-1.5.0}/tests/test_di.py +14 -4
- {aspyx-1.4.1 → aspyx-1.5.0}/tests/test_exception_manager.py +2 -2
- {aspyx-1.4.1 → aspyx-1.5.0}/tests/test_proxy.py +13 -0
- aspyx-1.4.1/PKG-INFO +0 -845
- aspyx-1.4.1/README.md +0 -811
- aspyx-1.4.1/pyproject.toml +0 -24
- aspyx-1.4.1/setup.cfg +0 -4
- aspyx-1.4.1/src/aspyx/__init__.py +0 -0
- aspyx-1.4.1/src/aspyx.egg-info/PKG-INFO +0 -845
- aspyx-1.4.1/src/aspyx.egg-info/SOURCES.txt +0 -35
- aspyx-1.4.1/src/aspyx.egg-info/dependency_links.txt +0 -1
- aspyx-1.4.1/src/aspyx.egg-info/requires.txt +0 -3
- aspyx-1.4.1/src/aspyx.egg-info/top_level.txt +0 -1
- {aspyx-1.4.1 → aspyx-1.5.0}/LICENSE +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/aop/__init__.py +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/aop/aop.py +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/configuration/__init__.py +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/configuration/configuration.py +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/configuration/env_configuration_source.py +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/configuration/yaml_configuration_source.py +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/threading/__init__.py +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/threading/synchronized.py +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/exception/__init__.py +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/reflection/__init__.py +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/threading/__init__.py +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/threading/thread_local.py +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/util/__init__.py +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/util/stringbuilder.py +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/tests/test_aop.py +0 -0
- {aspyx-1.4.1 → aspyx-1.5.0}/tests/test_reflection.py +0 -0
aspyx-1.5.0/.gitignore
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
|
|
110
|
+
# pdm
|
|
111
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
112
|
+
#pdm.lock
|
|
113
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
114
|
+
# in version control.
|
|
115
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
116
|
+
.pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
121
|
+
__pypackages__/
|
|
122
|
+
|
|
123
|
+
# Celery stuff
|
|
124
|
+
celerybeat-schedule
|
|
125
|
+
celerybeat.pid
|
|
126
|
+
|
|
127
|
+
# SageMath parsed files
|
|
128
|
+
*.sage.py
|
|
129
|
+
|
|
130
|
+
# Environments
|
|
131
|
+
.env
|
|
132
|
+
.venv
|
|
133
|
+
env/
|
|
134
|
+
venv/
|
|
135
|
+
ENV/
|
|
136
|
+
env.bak/
|
|
137
|
+
venv.bak/
|
|
138
|
+
|
|
139
|
+
# Spyder project settings
|
|
140
|
+
.spyderproject
|
|
141
|
+
.spyproject
|
|
142
|
+
|
|
143
|
+
# Rope project settings
|
|
144
|
+
.ropeproject
|
|
145
|
+
|
|
146
|
+
# mkdocs documentation
|
|
147
|
+
/site
|
|
148
|
+
|
|
149
|
+
# mypy
|
|
150
|
+
.mypy_cache/
|
|
151
|
+
.dmypy.json
|
|
152
|
+
dmypy.json
|
|
153
|
+
|
|
154
|
+
# Pyre type checker
|
|
155
|
+
.pyre/
|
|
156
|
+
|
|
157
|
+
# pytype static type analyzer
|
|
158
|
+
.pytype/
|
|
159
|
+
|
|
160
|
+
# Cython debug symbols
|
|
161
|
+
cython_debug/
|
|
162
|
+
|
|
163
|
+
# PyCharm
|
|
164
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
165
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
166
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
167
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
168
|
+
#.idea/
|
|
169
|
+
|
|
170
|
+
# Abstra
|
|
171
|
+
# Abstra is an AI-powered process automation framework.
|
|
172
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
173
|
+
# Learn more at https://abstra.io/docs
|
|
174
|
+
.abstra/
|
|
175
|
+
|
|
176
|
+
# Visual Studio Code
|
|
177
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
178
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
179
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
180
|
+
# you could uncomment the following to ignore the enitre vscode folder
|
|
181
|
+
# .vscode/
|
|
182
|
+
|
|
183
|
+
# Ruff stuff:
|
|
184
|
+
.ruff_cache/
|
|
185
|
+
|
|
186
|
+
# PyPI configuration file
|
|
187
|
+
.pypirc
|
|
188
|
+
|
|
189
|
+
# Cursor
|
|
190
|
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
|
191
|
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
|
192
|
+
# refer to https://docs.cursor.com/context/ignore-files
|
|
193
|
+
.cursorignore
|
|
194
|
+
.cursorindexingignore
|
aspyx-1.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aspyx
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: A DI and AOP library for Python
|
|
5
|
+
Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Andreas Ernst
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Requires-Dist: python-dotenv~=1.1.0
|
|
30
|
+
Requires-Dist: pyyaml~=6.0.2
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
aspyx
|
aspyx-1.5.0/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aspyx
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "aspyx"
|
|
3
|
+
version = "1.5.0"
|
|
4
|
+
description = "A DI and AOP library for Python"
|
|
5
|
+
authors = [{ name = "Andreas Ernst", email = "andreas.ernst7@gmail.com" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = { file = "LICENSE" }
|
|
8
|
+
requires-python = ">=3.9"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"python-dotenv~=1.1.0",
|
|
11
|
+
"pyyaml~=6.0.2"
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["hatchling"]
|
|
16
|
+
build-backend = "hatchling.build"
|
|
17
|
+
|
|
18
|
+
[tool.hatch.build]
|
|
19
|
+
source = "src"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/aspyx"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
This module provides dependency injection and aop capabilities for Python applications.
|
|
3
3
|
"""
|
|
4
|
-
from .di import conditional, requires_class, requires_feature, DIException, AbstractCallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory, module, inject, order, create, on_init, on_running, on_destroy, inject_environment, Factory, PostProcessor
|
|
4
|
+
from .di import InstanceProvider, conditional, requires_class, requires_feature, DIException, AbstractCallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory, module, inject, order, create, on_init, on_running, on_destroy, inject_environment, Factory, PostProcessor
|
|
5
5
|
|
|
6
6
|
# import something from the subpackages, so that the decorators are executed
|
|
7
7
|
|
|
@@ -21,7 +21,7 @@ __all__ = [
|
|
|
21
21
|
"inject",
|
|
22
22
|
"create",
|
|
23
23
|
"order",
|
|
24
|
-
|
|
24
|
+
"InstanceProvider",
|
|
25
25
|
"on_init",
|
|
26
26
|
"on_running",
|
|
27
27
|
"on_destroy",
|
|
@@ -316,13 +316,32 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
|
|
|
316
316
|
|
|
317
317
|
self.environment = environment
|
|
318
318
|
self.provider = provider
|
|
319
|
-
self.dependencies = []
|
|
319
|
+
self.dependencies : list[AbstractInstanceProvider] = []
|
|
320
320
|
self.scope_instance = Scopes.get(provider.get_scope(), environment)
|
|
321
321
|
|
|
322
|
+
# public
|
|
323
|
+
|
|
324
|
+
def print_tree(self, prefix=""):
|
|
325
|
+
children = self.dependencies
|
|
326
|
+
last_index = len(children) - 1
|
|
327
|
+
print(prefix + "+- " + self.report())
|
|
328
|
+
|
|
329
|
+
for i, child in enumerate(children):
|
|
330
|
+
if i == last_index:
|
|
331
|
+
# Last child
|
|
332
|
+
child_prefix = prefix + " "
|
|
333
|
+
else:
|
|
334
|
+
# Not last child
|
|
335
|
+
child_prefix = prefix + "| "
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
cast(EnvironmentInstanceProvider, child).print_tree(child_prefix)
|
|
339
|
+
|
|
322
340
|
# implement
|
|
323
341
|
|
|
324
342
|
def resolve(self, context: Providers.ResolveContext):
|
|
325
343
|
context.add(self)
|
|
344
|
+
context.push(self)
|
|
326
345
|
|
|
327
346
|
if not context.is_resolved(self):
|
|
328
347
|
context.provider_dependencies[self] = [] #?
|
|
@@ -332,7 +351,7 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
|
|
|
332
351
|
for type in type_and_params[0]:
|
|
333
352
|
if params > 0:
|
|
334
353
|
params -= 1
|
|
335
|
-
self.dependencies.append(context.get_provider(type))
|
|
354
|
+
self.dependencies.append(context.get_provider(type)) # try/catch TODO
|
|
336
355
|
|
|
337
356
|
provider = context.add_provider_dependency(self, type)
|
|
338
357
|
if provider is not None:
|
|
@@ -341,6 +360,8 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
|
|
|
341
360
|
else:
|
|
342
361
|
context.add(*context.get_provider_dependencies(self))
|
|
343
362
|
|
|
363
|
+
context.pop()
|
|
364
|
+
|
|
344
365
|
def get_module(self) -> str:
|
|
345
366
|
return self.provider.get_module()
|
|
346
367
|
|
|
@@ -403,7 +424,11 @@ class ClassInstanceProvider(InstanceProvider):
|
|
|
403
424
|
for method in TypeDescriptor.for_type(self.type).get_methods():
|
|
404
425
|
if method.has_decorator(inject):
|
|
405
426
|
for param in method.param_types:
|
|
427
|
+
if not Providers.is_registered(param):
|
|
428
|
+
raise DIRegistrationException(f"{self.type.__name__}.{method.method.__name__} declares an unknown parameter type {param.__name__}")
|
|
429
|
+
|
|
406
430
|
types.append(param)
|
|
431
|
+
# done
|
|
407
432
|
|
|
408
433
|
return types, self.params
|
|
409
434
|
|
|
@@ -431,28 +456,28 @@ class FunctionInstanceProvider(InstanceProvider):
|
|
|
431
456
|
|
|
432
457
|
# constructor
|
|
433
458
|
|
|
434
|
-
def __init__(self, clazz : Type, method
|
|
435
|
-
super().__init__(clazz, return_type, eager, scope)
|
|
459
|
+
def __init__(self, clazz : Type, method: TypeDescriptor.MethodDescriptor, eager = True, scope = "singleton"):
|
|
460
|
+
super().__init__(clazz, method.return_type, eager, scope)
|
|
436
461
|
|
|
437
|
-
self.method = method
|
|
462
|
+
self.method : TypeDescriptor.MethodDescriptor = method
|
|
438
463
|
|
|
439
464
|
# implement
|
|
440
465
|
|
|
441
466
|
def get_dependencies(self) -> (list[Type],int):
|
|
442
|
-
return [self.host], 1
|
|
467
|
+
return [self.host, *self.method.param_types], 1 + len(self.method.param_types)
|
|
443
468
|
|
|
444
469
|
def create(self, environment: Environment, *args):
|
|
445
470
|
Environment.logger.debug("%s create class %s", self, self.type.__qualname__)
|
|
446
471
|
|
|
447
|
-
instance = self.method(*args) # args[0]=self
|
|
472
|
+
instance = self.method.method(*args) # args[0]=self
|
|
448
473
|
|
|
449
474
|
return environment.created(instance)
|
|
450
475
|
|
|
451
476
|
def report(self) -> str:
|
|
452
|
-
return f"{self.host.__name__}.{self.method.__name__}"
|
|
477
|
+
return f"{self.host.__name__}.{self.method.get_name()}({', '.join(t.__name__ for t in self.method.param_types)}) -> {self.type.__qualname__}"
|
|
453
478
|
|
|
454
479
|
def __str__(self):
|
|
455
|
-
return f"FunctionInstanceProvider({self.host.__name__}.{self.method.__name__} -> {self.type.__name__})"
|
|
480
|
+
return f"FunctionInstanceProvider({self.host.__name__}.{self.method.get_name()}({', '.join(t.__name__ for t in self.method.param_types)}) -> {self.type.__name__})"
|
|
456
481
|
|
|
457
482
|
class FactoryInstanceProvider(InstanceProvider):
|
|
458
483
|
"""
|
|
@@ -483,7 +508,7 @@ class FactoryInstanceProvider(InstanceProvider):
|
|
|
483
508
|
return environment.created(args[0].create())
|
|
484
509
|
|
|
485
510
|
def report(self) -> str:
|
|
486
|
-
return f"{self.host.__name__}.create"
|
|
511
|
+
return f"{self.host.__name__}.create() -> {self.type.__name__} "
|
|
487
512
|
|
|
488
513
|
def __str__(self):
|
|
489
514
|
return f"FactoryInstanceProvider({self.host.__name__} -> {self.type.__name__})"
|
|
@@ -553,7 +578,8 @@ class Providers:
|
|
|
553
578
|
__slots__ = [
|
|
554
579
|
"dependencies",
|
|
555
580
|
"providers",
|
|
556
|
-
"provider_dependencies"
|
|
581
|
+
"provider_dependencies",
|
|
582
|
+
"path"
|
|
557
583
|
]
|
|
558
584
|
|
|
559
585
|
# constructor
|
|
@@ -561,6 +587,7 @@ class Providers:
|
|
|
561
587
|
def __init__(self, providers: Dict[Type, EnvironmentInstanceProvider]):
|
|
562
588
|
self.dependencies : list[EnvironmentInstanceProvider] = []
|
|
563
589
|
self.providers = providers
|
|
590
|
+
self.path = []
|
|
564
591
|
self.provider_dependencies : dict[EnvironmentInstanceProvider, list[EnvironmentInstanceProvider]] = {}
|
|
565
592
|
|
|
566
593
|
# public
|
|
@@ -586,7 +613,14 @@ class Providers:
|
|
|
586
613
|
|
|
587
614
|
return provider
|
|
588
615
|
|
|
616
|
+
def push(self, provider):
|
|
617
|
+
self.path.append(provider)
|
|
618
|
+
|
|
619
|
+
def pop(self):
|
|
620
|
+
self.path.pop()
|
|
621
|
+
|
|
589
622
|
def next(self):
|
|
623
|
+
self.path.clear()
|
|
590
624
|
self.dependencies.clear()
|
|
591
625
|
|
|
592
626
|
def get_provider(self, type: Type) -> EnvironmentInstanceProvider:
|
|
@@ -607,7 +641,7 @@ class Providers:
|
|
|
607
641
|
cycle = ""
|
|
608
642
|
|
|
609
643
|
first = True
|
|
610
|
-
for p in self.
|
|
644
|
+
for p in self.path:
|
|
611
645
|
if not first:
|
|
612
646
|
cycle += " -> "
|
|
613
647
|
|
|
@@ -638,6 +672,10 @@ class Providers:
|
|
|
638
672
|
else:
|
|
639
673
|
candidates.append(provider)
|
|
640
674
|
|
|
675
|
+
@classmethod
|
|
676
|
+
def is_registered(cls,type: Type) -> bool:
|
|
677
|
+
return Providers.providers.get(type, None) is not None
|
|
678
|
+
|
|
641
679
|
# add factories lazily
|
|
642
680
|
|
|
643
681
|
@classmethod
|
|
@@ -652,7 +690,7 @@ class Providers:
|
|
|
652
690
|
cache: Dict[Type,AbstractInstanceProvider] = {}
|
|
653
691
|
|
|
654
692
|
context: ConditionContext = {
|
|
655
|
-
"requires_feature":
|
|
693
|
+
"requires_feature": environment.has_feature,
|
|
656
694
|
"requires_class": lambda clazz : cache.get(clazz, None) is not None # ? only works if the class is in the cache already?
|
|
657
695
|
}
|
|
658
696
|
|
|
@@ -708,10 +746,13 @@ class Providers:
|
|
|
708
746
|
if type is provider.get_type():
|
|
709
747
|
raise ProviderCollisionException(f"type {type.__name__} already registered", existing_provider, provider)
|
|
710
748
|
|
|
711
|
-
if
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
749
|
+
if existing_provider.get_type() is not type:
|
|
750
|
+
# only overwrite if the existing provider is not specific
|
|
751
|
+
|
|
752
|
+
if isinstance(existing_provider, AmbiguousProvider):
|
|
753
|
+
cast(AmbiguousProvider, existing_provider).add_provider(provider)
|
|
754
|
+
else:
|
|
755
|
+
cache[type] = AmbiguousProvider(type, existing_provider, provider)
|
|
715
756
|
|
|
716
757
|
# recursion
|
|
717
758
|
|
|
@@ -763,8 +804,7 @@ def register_factories(cls: Type):
|
|
|
763
804
|
if return_type is None:
|
|
764
805
|
raise DIRegistrationException(f"{cls.__name__}.{method.method.__name__} expected to have a return type")
|
|
765
806
|
|
|
766
|
-
Providers.register(FunctionInstanceProvider(cls, method
|
|
767
|
-
create_decorator.args[1]))
|
|
807
|
+
Providers.register(FunctionInstanceProvider(cls, method, create_decorator.args[0], create_decorator.args[1]))
|
|
768
808
|
def order(prio = 0):
|
|
769
809
|
def decorator(cls):
|
|
770
810
|
Decorators.add(cls, order, prio)
|
|
@@ -939,12 +979,12 @@ class Environment:
|
|
|
939
979
|
class Foo:
|
|
940
980
|
def __init__(self):
|
|
941
981
|
|
|
942
|
-
@
|
|
943
|
-
class
|
|
982
|
+
@module()
|
|
983
|
+
class Module:
|
|
944
984
|
def __init__(self):
|
|
945
985
|
pass
|
|
946
986
|
|
|
947
|
-
environment = Environment(
|
|
987
|
+
environment = Environment(Module)
|
|
948
988
|
|
|
949
989
|
foo = environment.get(Foo) # will create an instance of Foo
|
|
950
990
|
```
|
|
@@ -1026,9 +1066,33 @@ class Environment:
|
|
|
1026
1066
|
|
|
1027
1067
|
def get_type_package(type: Type):
|
|
1028
1068
|
module_name = type.__module__
|
|
1029
|
-
module = sys.modules
|
|
1069
|
+
module = sys.modules.get(module_name)
|
|
1070
|
+
|
|
1071
|
+
if not module:
|
|
1072
|
+
raise ImportError(f"Module {module_name} not found")
|
|
1073
|
+
|
|
1074
|
+
# Try to get the package
|
|
1075
|
+
|
|
1076
|
+
package = getattr(module, '__package__', None)
|
|
1030
1077
|
|
|
1031
|
-
|
|
1078
|
+
# Fallback: if module is __main__, try to infer from the module name if possible
|
|
1079
|
+
|
|
1080
|
+
if not package:
|
|
1081
|
+
if module_name == '__main__':
|
|
1082
|
+
# Try to resolve real name via __file__
|
|
1083
|
+
path = getattr(module, '__file__', None)
|
|
1084
|
+
if path:
|
|
1085
|
+
Environment.logger.warning(
|
|
1086
|
+
"Module is __main__; consider running via -m to preserve package context")
|
|
1087
|
+
return ''
|
|
1088
|
+
|
|
1089
|
+
# Try to infer package name from module name
|
|
1090
|
+
|
|
1091
|
+
parts = module_name.split('.')
|
|
1092
|
+
if len(parts) > 1:
|
|
1093
|
+
return '.'.join(parts[:-1])
|
|
1094
|
+
|
|
1095
|
+
return package or ''
|
|
1032
1096
|
|
|
1033
1097
|
def import_package(name: str):
|
|
1034
1098
|
"""Import a package and all its submodules recursively."""
|
|
@@ -1106,6 +1170,9 @@ class Environment:
|
|
|
1106
1170
|
# construct eager objects for local providers
|
|
1107
1171
|
|
|
1108
1172
|
for provider in set(self.providers.values()):
|
|
1173
|
+
if isinstance(provider, EnvironmentInstanceProvider):
|
|
1174
|
+
provider.print_tree()
|
|
1175
|
+
|
|
1109
1176
|
if provider.is_eager():
|
|
1110
1177
|
provider.create(self)
|
|
1111
1178
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Dynamic proxies for method interception and delegation.
|
|
3
3
|
"""
|
|
4
|
-
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Generic, TypeVar, Type, Callable
|
|
5
6
|
|
|
6
7
|
T = TypeVar("T")
|
|
7
8
|
|
|
@@ -28,9 +29,18 @@ class DynamicProxy(Generic[T]):
|
|
|
28
29
|
# inner class
|
|
29
30
|
|
|
30
31
|
class Invocation:
|
|
31
|
-
|
|
32
|
+
__slots__ = [
|
|
33
|
+
"type",
|
|
34
|
+
"method",
|
|
35
|
+
"args",
|
|
36
|
+
"kwargs",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
# constructor
|
|
40
|
+
|
|
41
|
+
def __init__(self, type: Type[T], method: Callable, *args, **kwargs):
|
|
32
42
|
self.type = type
|
|
33
|
-
self.
|
|
43
|
+
self.method = method
|
|
34
44
|
self.args = args
|
|
35
45
|
self.kwargs = kwargs
|
|
36
46
|
|
|
@@ -38,12 +48,20 @@ class DynamicProxy(Generic[T]):
|
|
|
38
48
|
def invoke(self, invocation: 'DynamicProxy.Invocation'):
|
|
39
49
|
pass
|
|
40
50
|
|
|
51
|
+
async def invoke_async(self, invocation: 'DynamicProxy.Invocation'):
|
|
52
|
+
return self.invoke(invocation)
|
|
53
|
+
|
|
41
54
|
# class methods
|
|
42
55
|
|
|
43
56
|
@classmethod
|
|
44
57
|
def create(cls, type: Type[T], invocation_handler: 'DynamicProxy.InvocationHandler') -> T:
|
|
45
58
|
return DynamicProxy(type, invocation_handler)
|
|
46
59
|
|
|
60
|
+
__slots__ = [
|
|
61
|
+
"type",
|
|
62
|
+
"invocation_handler"
|
|
63
|
+
]
|
|
64
|
+
|
|
47
65
|
# constructor
|
|
48
66
|
|
|
49
67
|
def __init__(self, type: Type[T], invocation_handler: 'DynamicProxy.InvocationHandler'):
|
|
@@ -53,7 +71,16 @@ class DynamicProxy(Generic[T]):
|
|
|
53
71
|
# public
|
|
54
72
|
|
|
55
73
|
def __getattr__(self, name):
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
method = getattr(self.type, name)
|
|
75
|
+
|
|
76
|
+
if inspect.iscoroutinefunction(method):
|
|
77
|
+
async def async_wrapper(*args, **kwargs):
|
|
78
|
+
return await self.invocation_handler.invoke_async(DynamicProxy.Invocation(self.type, method, *args, **kwargs))
|
|
79
|
+
|
|
80
|
+
return async_wrapper
|
|
81
|
+
|
|
82
|
+
else:
|
|
83
|
+
def sync_wrapper(*args, **kwargs):
|
|
84
|
+
return self.invocation_handler.invoke(DynamicProxy.Invocation(self.type, method, *args, **kwargs))
|
|
58
85
|
|
|
59
|
-
|
|
86
|
+
return sync_wrapper
|
|
@@ -5,8 +5,10 @@ including their methods, decorators, and type hints. It supports caching for per
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import inspect
|
|
8
|
-
from inspect import signature
|
|
8
|
+
from inspect import signature
|
|
9
9
|
import threading
|
|
10
|
+
from types import FunctionType
|
|
11
|
+
|
|
10
12
|
from typing import Callable, get_type_hints, Type, Dict, Optional
|
|
11
13
|
from weakref import WeakKeyDictionary
|
|
12
14
|
|
|
@@ -25,7 +27,7 @@ class DecoratorDescriptor:
|
|
|
25
27
|
self.args = args
|
|
26
28
|
|
|
27
29
|
def __str__(self):
|
|
28
|
-
return f"@{self.decorator.__name__}({','.join(self.args)})"
|
|
30
|
+
return f"@{self.decorator.__name__}({', '.join(map(str, self.args))})"
|
|
29
31
|
|
|
30
32
|
class Decorators:
|
|
31
33
|
"""
|
|
@@ -59,6 +61,14 @@ class Decorators:
|
|
|
59
61
|
"""
|
|
60
62
|
return any(decorator.decorator is callable for decorator in Decorators.get(func_or_class))
|
|
61
63
|
|
|
64
|
+
@classmethod
|
|
65
|
+
def get_decorator(cls, func_or_class, callable: Callable) -> DecoratorDescriptor:
|
|
66
|
+
return next((decorator for decorator in Decorators.get_all(func_or_class) if decorator.decorator is callable), None)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def get_all(cls, func_or_class) -> list[DecoratorDescriptor]:
|
|
70
|
+
return getattr(func_or_class, '__decorators__', [])
|
|
71
|
+
|
|
62
72
|
@classmethod
|
|
63
73
|
def get(cls, func_or_class) -> list[DecoratorDescriptor]:
|
|
64
74
|
"""
|
|
@@ -67,9 +77,14 @@ class Decorators:
|
|
|
67
77
|
func_or_class: the function or class
|
|
68
78
|
|
|
69
79
|
Returns:
|
|
70
|
-
list[DecoratorDescriptor]:
|
|
80
|
+
list[DecoratorDescriptor]: the list
|
|
71
81
|
"""
|
|
72
|
-
|
|
82
|
+
if inspect.ismethod(func_or_class):
|
|
83
|
+
func_or_class = func_or_class.__func__ # unwrap bound method
|
|
84
|
+
|
|
85
|
+
#return getattr(func_or_class, '__decorators__', []) will return inherited as well
|
|
86
|
+
return func_or_class.__dict__.get('__decorators__', [])
|
|
87
|
+
|
|
73
88
|
|
|
74
89
|
class TypeDescriptor:
|
|
75
90
|
"""
|
|
@@ -130,6 +145,9 @@ class TypeDescriptor:
|
|
|
130
145
|
"""
|
|
131
146
|
return inspect.iscoroutinefunction(self.method)
|
|
132
147
|
|
|
148
|
+
def get_decorators(self) -> list[DecoratorDescriptor]:
|
|
149
|
+
return self.decorators
|
|
150
|
+
|
|
133
151
|
def get_decorator(self, decorator: Callable) -> Optional[DecoratorDescriptor]:
|
|
134
152
|
"""
|
|
135
153
|
return the DecoratorDescriptor - if any - associated with the passed Callable
|
|
@@ -212,10 +230,16 @@ class TypeDescriptor:
|
|
|
212
230
|
# internal
|
|
213
231
|
|
|
214
232
|
def _get_local_members(self, cls):
|
|
233
|
+
#return [
|
|
234
|
+
# (name, value)
|
|
235
|
+
# for name, value in getmembers(cls, predicate=inspect.isfunction)
|
|
236
|
+
# if name in cls.__dict__
|
|
237
|
+
#]
|
|
238
|
+
|
|
215
239
|
return [
|
|
216
|
-
(name,
|
|
217
|
-
for name,
|
|
218
|
-
if
|
|
240
|
+
(name, attr)
|
|
241
|
+
for name, attr in cls.__dict__.items()
|
|
242
|
+
if isinstance(attr, FunctionType)
|
|
219
243
|
]
|
|
220
244
|
|
|
221
245
|
# public
|