aspyx 1.4.1__tar.gz → 1.5.1__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.

Files changed (44) hide show
  1. aspyx-1.5.1/.gitignore +194 -0
  2. {aspyx-1.4.1/src/aspyx.egg-info → aspyx-1.5.1}/PKG-INFO +7 -13
  3. {aspyx-1.4.1 → aspyx-1.5.1}/README.md +3 -7
  4. aspyx-1.5.1/pyproject.toml +22 -0
  5. aspyx-1.5.1/src/aspyx/__init__.py +1 -0
  6. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/di/__init__.py +2 -2
  7. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/di/di.py +105 -80
  8. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/exception/exception_manager.py +1 -1
  9. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/reflection/proxy.py +33 -6
  10. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/reflection/reflection.py +31 -7
  11. aspyx-1.5.1/tests/config.yaml +4 -0
  12. aspyx-1.5.1/tests/config1.yaml +4 -0
  13. aspyx-1.5.1/tests/di_import.py +14 -0
  14. aspyx-1.5.1/tests/sub_import.py +14 -0
  15. {aspyx-1.4.1 → aspyx-1.5.1}/tests/test_configuration.py +2 -2
  16. {aspyx-1.4.1 → aspyx-1.5.1}/tests/test_cycle.py +11 -1
  17. {aspyx-1.4.1 → aspyx-1.5.1}/tests/test_di.py +14 -4
  18. {aspyx-1.4.1 → aspyx-1.5.1}/tests/test_exception_manager.py +2 -2
  19. {aspyx-1.4.1 → aspyx-1.5.1}/tests/test_proxy.py +13 -0
  20. aspyx-1.4.1/PKG-INFO +0 -845
  21. aspyx-1.4.1/pyproject.toml +0 -24
  22. aspyx-1.4.1/setup.cfg +0 -4
  23. aspyx-1.4.1/src/aspyx/__init__.py +0 -0
  24. aspyx-1.4.1/src/aspyx.egg-info/SOURCES.txt +0 -35
  25. aspyx-1.4.1/src/aspyx.egg-info/dependency_links.txt +0 -1
  26. aspyx-1.4.1/src/aspyx.egg-info/requires.txt +0 -3
  27. aspyx-1.4.1/src/aspyx.egg-info/top_level.txt +0 -1
  28. {aspyx-1.4.1 → aspyx-1.5.1}/LICENSE +0 -0
  29. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/di/aop/__init__.py +0 -0
  30. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/di/aop/aop.py +0 -0
  31. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/di/configuration/__init__.py +0 -0
  32. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/di/configuration/configuration.py +0 -0
  33. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/di/configuration/env_configuration_source.py +0 -0
  34. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/di/configuration/yaml_configuration_source.py +0 -0
  35. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/di/threading/__init__.py +0 -0
  36. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/di/threading/synchronized.py +0 -0
  37. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/exception/__init__.py +0 -0
  38. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/reflection/__init__.py +0 -0
  39. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/threading/__init__.py +0 -0
  40. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/threading/thread_local.py +0 -0
  41. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/util/__init__.py +0 -0
  42. {aspyx-1.4.1 → aspyx-1.5.1}/src/aspyx/util/stringbuilder.py +0 -0
  43. {aspyx-1.4.1 → aspyx-1.5.1}/tests/test_aop.py +0 -0
  44. {aspyx-1.4.1 → aspyx-1.5.1}/tests/test_reflection.py +0 -0
aspyx-1.5.1/.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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aspyx
3
- Version: 1.4.1
3
+ Version: 1.5.1
4
4
  Summary: A DI and AOP library for Python
5
5
  Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
6
6
  License: MIT License
@@ -24,13 +24,11 @@ License: MIT License
24
24
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
25
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
26
  SOFTWARE.
27
-
27
+ License-File: LICENSE
28
28
  Requires-Python: >=3.9
29
+ Requires-Dist: python-dotenv~=1.1.0
30
+ Requires-Dist: pyyaml~=6.0.2
29
31
  Description-Content-Type: text/markdown
30
- License-File: LICENSE
31
- Provides-Extra: dev
32
- Requires-Dist: mkdocstrings-python; extra == "dev"
33
- Dynamic: license-file
34
32
 
35
33
  # aspyx
36
34
 
@@ -73,7 +71,7 @@ Dynamic: license-file
73
71
 
74
72
  While working on AI-related projects in Python, I was looking for a dependency injection (DI) framework. After evaluating existing options, my impression was that the most either lacked key features — such as integrated AOP — or had APIs that felt overly technical and complex, which made me develop a library on my own with the following goals
75
73
 
76
- - bring both di and AOP features together in a lightweight library ( still only about 2T loc),
74
+ - bring both di and AOP features together in a lightweight library,
77
75
  - be as minimal invasive as possible,
78
76
  - offering mechanisms to easily extend and customize features without touching the core,
79
77
  - while still offering a _simple_ and _readable_ api that doesnt overwhelm developers and only requires a minimum initial learning curve
@@ -85,7 +83,7 @@ The AOP integration, in particular, makes a lot of sense because:
85
83
 
86
84
  # Overview
87
85
 
88
- Aspyx is a lightweight - still only about 2T LOC- Python library that provides both Dependency Injection (DI) and Aspect-Oriented Programming (AOP) support.
86
+ Aspyx is a lightweight - still only about 2K LOC - Python library that provides both Dependency Injection (DI) and Aspect-Oriented Programming (AOP) support.
89
87
 
90
88
  The following DI features are supported
91
89
  - constructor and setter injection
@@ -100,7 +98,7 @@ The following DI features are supported
100
98
  - lifecycle events methods `on_init`, `on_destroy`, `on_running`
101
99
  - Automatic discovery and bundling of injectable objects based on their module location, including support for recursive imports
102
100
  - Instantiation of one or possible more isolated container instances — called environments — each managing the lifecycle of a related set of objects,
103
- - Support for hierarchical environments, enabling structured scoping and layered object management.
101
+ - Support for hierarchical environments, enabling structured scoping and layered object management.
104
102
 
105
103
  With respect to AOP:
106
104
  - support for before, around, after and error aspects
@@ -839,7 +837,3 @@ class ExceptionAdvice:
839
837
  **1.4.1**
840
838
 
841
839
  - mkdocs
842
-
843
-
844
-
845
-
@@ -39,7 +39,7 @@
39
39
 
40
40
  While working on AI-related projects in Python, I was looking for a dependency injection (DI) framework. After evaluating existing options, my impression was that the most either lacked key features — such as integrated AOP — or had APIs that felt overly technical and complex, which made me develop a library on my own with the following goals
41
41
 
42
- - bring both di and AOP features together in a lightweight library ( still only about 2T loc),
42
+ - bring both di and AOP features together in a lightweight library,
43
43
  - be as minimal invasive as possible,
44
44
  - offering mechanisms to easily extend and customize features without touching the core,
45
45
  - while still offering a _simple_ and _readable_ api that doesnt overwhelm developers and only requires a minimum initial learning curve
@@ -51,7 +51,7 @@ The AOP integration, in particular, makes a lot of sense because:
51
51
 
52
52
  # Overview
53
53
 
54
- Aspyx is a lightweight - still only about 2T LOC- Python library that provides both Dependency Injection (DI) and Aspect-Oriented Programming (AOP) support.
54
+ Aspyx is a lightweight - still only about 2K LOC - Python library that provides both Dependency Injection (DI) and Aspect-Oriented Programming (AOP) support.
55
55
 
56
56
  The following DI features are supported
57
57
  - constructor and setter injection
@@ -66,7 +66,7 @@ The following DI features are supported
66
66
  - lifecycle events methods `on_init`, `on_destroy`, `on_running`
67
67
  - Automatic discovery and bundling of injectable objects based on their module location, including support for recursive imports
68
68
  - Instantiation of one or possible more isolated container instances — called environments — each managing the lifecycle of a related set of objects,
69
- - Support for hierarchical environments, enabling structured scoping and layered object management.
69
+ - Support for hierarchical environments, enabling structured scoping and layered object management.
70
70
 
71
71
  With respect to AOP:
72
72
  - support for before, around, after and error aspects
@@ -805,7 +805,3 @@ class ExceptionAdvice:
805
805
  **1.4.1**
806
806
 
807
807
  - mkdocs
808
-
809
-
810
-
811
-
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "aspyx"
3
+ version = "1.5.1"
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,30 +316,44 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
316
316
 
317
317
  self.environment = environment
318
318
  self.provider = provider
319
- self.dependencies = []
319
+ self.dependencies : Optional[list[AbstractInstanceProvider]] = None # FOO
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
- context.add(self)
326
-
327
- if not context.is_resolved(self):
328
- context.provider_dependencies[self] = [] #?
343
+ if self.dependencies is None:
344
+ self.dependencies = []
345
+ context.push(self)
346
+ try:
347
+ type_and_params = self.provider.get_dependencies()
348
+ #params = type_and_params[1]
349
+ for type in type_and_params[0]:
350
+ provider = context.require_provider(type)
329
351
 
330
- type_and_params = self.provider.get_dependencies()
331
- params = type_and_params[1]
332
- for type in type_and_params[0]:
333
- if params > 0:
334
- params -= 1
335
- self.dependencies.append(context.get_provider(type))
352
+ self.dependencies.append(provider)
336
353
 
337
- provider = context.add_provider_dependency(self, type)
338
- if provider is not None:
339
354
  provider.resolve(context)
340
-
341
- else:
342
- context.add(*context.get_provider_dependencies(self))
355
+ finally:
356
+ context.pop()
343
357
 
344
358
  def get_module(self) -> str:
345
359
  return self.provider.get_module()
@@ -403,7 +417,11 @@ class ClassInstanceProvider(InstanceProvider):
403
417
  for method in TypeDescriptor.for_type(self.type).get_methods():
404
418
  if method.has_decorator(inject):
405
419
  for param in method.param_types:
420
+ if not Providers.is_registered(param):
421
+ raise DIRegistrationException(f"{self.type.__name__}.{method.method.__name__} declares an unknown parameter type {param.__name__}")
422
+
406
423
  types.append(param)
424
+ # done
407
425
 
408
426
  return types, self.params
409
427
 
@@ -431,28 +449,28 @@ class FunctionInstanceProvider(InstanceProvider):
431
449
 
432
450
  # constructor
433
451
 
434
- def __init__(self, clazz : Type, method, return_type : Type[T], eager = True, scope = "singleton"):
435
- super().__init__(clazz, return_type, eager, scope)
452
+ def __init__(self, clazz : Type, method: TypeDescriptor.MethodDescriptor, eager = True, scope = "singleton"):
453
+ super().__init__(clazz, method.return_type, eager, scope)
436
454
 
437
- self.method = method
455
+ self.method : TypeDescriptor.MethodDescriptor = method
438
456
 
439
457
  # implement
440
458
 
441
459
  def get_dependencies(self) -> (list[Type],int):
442
- return [self.host], 1
460
+ return [self.host, *self.method.param_types], 1 + len(self.method.param_types)
443
461
 
444
462
  def create(self, environment: Environment, *args):
445
463
  Environment.logger.debug("%s create class %s", self, self.type.__qualname__)
446
464
 
447
- instance = self.method(*args) # args[0]=self
465
+ instance = self.method.method(*args) # args[0]=self
448
466
 
449
467
  return environment.created(instance)
450
468
 
451
469
  def report(self) -> str:
452
- return f"{self.host.__name__}.{self.method.__name__}"
470
+ return f"{self.host.__name__}.{self.method.get_name()}({', '.join(t.__name__ for t in self.method.param_types)}) -> {self.type.__qualname__}"
453
471
 
454
472
  def __str__(self):
455
- return f"FunctionInstanceProvider({self.host.__name__}.{self.method.__name__} -> {self.type.__name__})"
473
+ return f"FunctionInstanceProvider({self.host.__name__}.{self.method.get_name()}({', '.join(t.__name__ for t in self.method.param_types)}) -> {self.type.__name__})"
456
474
 
457
475
  class FactoryInstanceProvider(InstanceProvider):
458
476
  """
@@ -483,7 +501,7 @@ class FactoryInstanceProvider(InstanceProvider):
483
501
  return environment.created(args[0].create())
484
502
 
485
503
  def report(self) -> str:
486
- return f"{self.host.__name__}.create"
504
+ return f"{self.host.__name__}.create() -> {self.type.__name__} "
487
505
 
488
506
  def __str__(self):
489
507
  return f"FactoryInstanceProvider({self.host.__name__} -> {self.type.__name__})"
@@ -545,69 +563,45 @@ class PostProcessor(LifecycleProcessor):
545
563
 
546
564
  class Providers:
547
565
  """
548
- The Providers class is a static class that manages the registration and resolution of InstanceProviders.
566
+ The Providers class is a static class used in the context of the registration and resolution of InstanceProviders.
549
567
  """
550
568
  # local class
551
569
 
552
570
  class ResolveContext:
553
571
  __slots__ = [
554
- "dependencies",
555
572
  "providers",
556
- "provider_dependencies"
573
+ "path"
557
574
  ]
558
575
 
559
576
  # constructor
560
577
 
561
578
  def __init__(self, providers: Dict[Type, EnvironmentInstanceProvider]):
562
- self.dependencies : list[EnvironmentInstanceProvider] = []
563
579
  self.providers = providers
564
- self.provider_dependencies : dict[EnvironmentInstanceProvider, list[EnvironmentInstanceProvider]] = {}
580
+ self.path = []
565
581
 
566
582
  # public
567
583
 
568
- def is_resolved(self, provider: EnvironmentInstanceProvider) -> bool:
569
- return self.provider_dependencies.get(provider, None) is not None
570
-
571
- def get_provider_dependencies(self, provider: EnvironmentInstanceProvider) -> list[EnvironmentInstanceProvider]:
572
- return self.provider_dependencies[provider]
573
-
574
- def add_provider_dependency(self, provider: EnvironmentInstanceProvider, type: Type) -> Optional[EnvironmentInstanceProvider]:
575
- provider_dependencies = self.provider_dependencies.get(provider, None)
576
- if provider_dependencies is None:
577
- provider_dependencies = []
578
- self.provider_dependencies[provider] = provider_dependencies
584
+ def push(self, provider):
585
+ self.path.append(provider)
579
586
 
580
- provider = self.get_provider(type)
587
+ def pop(self):
588
+ self.path.pop()
581
589
 
582
- if any(issubclass(provider.get_type(), dependency.get_type()) for dependency in provider_dependencies):
583
- return None
584
-
585
- provider_dependencies.append(provider)
586
-
587
- return provider
588
-
589
- def next(self):
590
- self.dependencies.clear()
591
-
592
- def get_provider(self, type: Type) -> EnvironmentInstanceProvider:
590
+ def require_provider(self, type: Type) -> EnvironmentInstanceProvider:
593
591
  provider = self.providers.get(type, None)
594
592
  if provider is None:
595
593
  raise DIRegistrationException(f"Provider for {type} is not defined")
596
594
 
597
- return provider
595
+ if provider in self.path:
596
+ raise DIRegistrationException(self.cycle_report(provider))
598
597
 
599
- def add(self, *providers: EnvironmentInstanceProvider):
600
- for provider in providers:
601
- if next((p for p in self.dependencies if p.get_type() is provider.get_type()), None) is not None:
602
- raise DIRegistrationException(self.cycle_report(provider))
603
-
604
- self.dependencies.append(provider)
598
+ return provider
605
599
 
606
600
  def cycle_report(self, provider: AbstractInstanceProvider):
607
601
  cycle = ""
608
602
 
609
603
  first = True
610
- for p in self.dependencies:
604
+ for p in self.path:
611
605
  if not first:
612
606
  cycle += " -> "
613
607
 
@@ -638,6 +632,10 @@ class Providers:
638
632
  else:
639
633
  candidates.append(provider)
640
634
 
635
+ @classmethod
636
+ def is_registered(cls,type: Type) -> bool:
637
+ return Providers.providers.get(type, None) is not None
638
+
641
639
  # add factories lazily
642
640
 
643
641
  @classmethod
@@ -652,7 +650,7 @@ class Providers:
652
650
  cache: Dict[Type,AbstractInstanceProvider] = {}
653
651
 
654
652
  context: ConditionContext = {
655
- "requires_feature": lambda feature : environment.has_feature(feature),
653
+ "requires_feature": environment.has_feature,
656
654
  "requires_class": lambda clazz : cache.get(clazz, None) is not None # ? only works if the class is in the cache already?
657
655
  }
658
656
 
@@ -708,10 +706,13 @@ class Providers:
708
706
  if type is provider.get_type():
709
707
  raise ProviderCollisionException(f"type {type.__name__} already registered", existing_provider, provider)
710
708
 
711
- if isinstance(existing_provider, AmbiguousProvider):
712
- cast(AmbiguousProvider, existing_provider).add_provider(provider)
713
- else:
714
- cache[type] = AmbiguousProvider(type, existing_provider, provider)
709
+ if existing_provider.get_type() is not type:
710
+ # only overwrite if the existing provider is not specific
711
+
712
+ if isinstance(existing_provider, AmbiguousProvider):
713
+ cast(AmbiguousProvider, existing_provider).add_provider(provider)
714
+ else:
715
+ cache[type] = AmbiguousProvider(type, existing_provider, provider)
715
716
 
716
717
  # recursion
717
718
 
@@ -747,7 +748,6 @@ class Providers:
747
748
  provider_context = Providers.ResolveContext(providers)
748
749
  for provider in mapped.values():
749
750
  provider.resolve(provider_context)
750
- provider_context.next() # clear dependencies
751
751
 
752
752
  # done
753
753
 
@@ -763,8 +763,7 @@ def register_factories(cls: Type):
763
763
  if return_type is None:
764
764
  raise DIRegistrationException(f"{cls.__name__}.{method.method.__name__} expected to have a return type")
765
765
 
766
- Providers.register(FunctionInstanceProvider(cls, method.method, return_type, create_decorator.args[0],
767
- create_decorator.args[1]))
766
+ Providers.register(FunctionInstanceProvider(cls, method, create_decorator.args[0], create_decorator.args[1]))
768
767
  def order(prio = 0):
769
768
  def decorator(cls):
770
769
  Decorators.add(cls, order, prio)
@@ -939,12 +938,12 @@ class Environment:
939
938
  class Foo:
940
939
  def __init__(self):
941
940
 
942
- @environment()
943
- class SimpleEnvironment:
941
+ @module()
942
+ class Module:
944
943
  def __init__(self):
945
944
  pass
946
945
 
947
- environment = Environment(SimpleEnvironment)
946
+ environment = Environment(Module)
948
947
 
949
948
  foo = environment.get(Foo) # will create an instance of Foo
950
949
  ```
@@ -976,6 +975,11 @@ class Environment:
976
975
  parent (Optional[Environment]): Optional parent environment, whose objects are inherited.
977
976
  """
978
977
 
978
+ def add_provider(type: Type, provider: AbstractInstanceProvider):
979
+ Environment.logger.debug("\tadd provider %s for %s", provider, type)
980
+
981
+ self.providers[type] = provider
982
+
979
983
  Environment.logger.debug("create environment for class %s", env.__qualname__)
980
984
 
981
985
  # initialize
@@ -996,9 +1000,11 @@ class Environment:
996
1000
  for provider_type, inherited_provider in self.parent.providers.items():
997
1001
  if inherited_provider.get_scope() == "environment":
998
1002
  # replace with own environment instance provider
999
- self.providers[provider_type] = EnvironmentInstanceProvider(self, cast(EnvironmentInstanceProvider, inherited_provider).provider)
1003
+ provider = EnvironmentInstanceProvider(self, cast(EnvironmentInstanceProvider, inherited_provider).provider)
1004
+ provider.dependencies = [] # ??
1005
+ add_provider(provider_type, provider)
1000
1006
  else:
1001
- self.providers[provider_type] = inherited_provider
1007
+ add_provider(provider_type, inherited_provider)
1002
1008
 
1003
1009
  # inherit processors as is unless they have an environment scope
1004
1010
 
@@ -1019,16 +1025,35 @@ class Environment:
1019
1025
 
1020
1026
  loaded = set()
1021
1027
 
1022
- def add_provider(type: Type, provider: AbstractInstanceProvider):
1023
- Environment.logger.debug("\tadd provider %s for %s", provider, type)
1024
-
1025
- self.providers[type] = provider
1026
-
1027
1028
  def get_type_package(type: Type):
1028
1029
  module_name = type.__module__
1029
- module = sys.modules[module_name]
1030
+ module = sys.modules.get(module_name)
1031
+
1032
+ if not module:
1033
+ raise ImportError(f"Module {module_name} not found")
1034
+
1035
+ # Try to get the package
1036
+
1037
+ package = getattr(module, '__package__', None)
1038
+
1039
+ # Fallback: if module is __main__, try to infer from the module name if possible
1040
+
1041
+ if not package:
1042
+ if module_name == '__main__':
1043
+ # Try to resolve real name via __file__
1044
+ path = getattr(module, '__file__', None)
1045
+ if path:
1046
+ Environment.logger.warning(
1047
+ "Module is __main__; consider running via -m to preserve package context")
1048
+ return ''
1049
+
1050
+ # Try to infer package name from module name
1051
+
1052
+ parts = module_name.split('.')
1053
+ if len(parts) > 1:
1054
+ return '.'.join(parts[:-1])
1030
1055
 
1031
- return module.__package__
1056
+ return package or ''
1032
1057
 
1033
1058
  def import_package(name: str):
1034
1059
  """Import a package and all its submodules recursively."""
@@ -155,7 +155,7 @@ class ExceptionManager:
155
155
 
156
156
  # chain
157
157
 
158
- for i in range(0, len(chain) - 2):
158
+ for i in range(0, len(chain) - 1):
159
159
  chain[i].next = chain[i + 1]
160
160
 
161
161
  if len(chain) > 0: