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.

Files changed (46) hide show
  1. aspyx-1.5.0/.gitignore +194 -0
  2. aspyx-1.5.0/PKG-INFO +33 -0
  3. aspyx-1.5.0/README.md +1 -0
  4. aspyx-1.5.0/pyproject.toml +22 -0
  5. aspyx-1.5.0/src/aspyx/__init__.py +1 -0
  6. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/__init__.py +2 -2
  7. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/di.py +91 -24
  8. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/exception/exception_manager.py +1 -1
  9. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/reflection/proxy.py +33 -6
  10. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/reflection/reflection.py +31 -7
  11. aspyx-1.5.0/tests/config.yaml +4 -0
  12. aspyx-1.5.0/tests/config1.yaml +4 -0
  13. aspyx-1.5.0/tests/di_import.py +14 -0
  14. aspyx-1.5.0/tests/sub_import.py +14 -0
  15. {aspyx-1.4.1 → aspyx-1.5.0}/tests/test_configuration.py +2 -2
  16. {aspyx-1.4.1 → aspyx-1.5.0}/tests/test_cycle.py +11 -1
  17. {aspyx-1.4.1 → aspyx-1.5.0}/tests/test_di.py +14 -4
  18. {aspyx-1.4.1 → aspyx-1.5.0}/tests/test_exception_manager.py +2 -2
  19. {aspyx-1.4.1 → aspyx-1.5.0}/tests/test_proxy.py +13 -0
  20. aspyx-1.4.1/PKG-INFO +0 -845
  21. aspyx-1.4.1/README.md +0 -811
  22. aspyx-1.4.1/pyproject.toml +0 -24
  23. aspyx-1.4.1/setup.cfg +0 -4
  24. aspyx-1.4.1/src/aspyx/__init__.py +0 -0
  25. aspyx-1.4.1/src/aspyx.egg-info/PKG-INFO +0 -845
  26. aspyx-1.4.1/src/aspyx.egg-info/SOURCES.txt +0 -35
  27. aspyx-1.4.1/src/aspyx.egg-info/dependency_links.txt +0 -1
  28. aspyx-1.4.1/src/aspyx.egg-info/requires.txt +0 -3
  29. aspyx-1.4.1/src/aspyx.egg-info/top_level.txt +0 -1
  30. {aspyx-1.4.1 → aspyx-1.5.0}/LICENSE +0 -0
  31. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/aop/__init__.py +0 -0
  32. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/aop/aop.py +0 -0
  33. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/configuration/__init__.py +0 -0
  34. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/configuration/configuration.py +0 -0
  35. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/configuration/env_configuration_source.py +0 -0
  36. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/configuration/yaml_configuration_source.py +0 -0
  37. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/threading/__init__.py +0 -0
  38. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/di/threading/synchronized.py +0 -0
  39. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/exception/__init__.py +0 -0
  40. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/reflection/__init__.py +0 -0
  41. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/threading/__init__.py +0 -0
  42. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/threading/thread_local.py +0 -0
  43. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/util/__init__.py +0 -0
  44. {aspyx-1.4.1 → aspyx-1.5.0}/src/aspyx/util/stringbuilder.py +0 -0
  45. {aspyx-1.4.1 → aspyx-1.5.0}/tests/test_aop.py +0 -0
  46. {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, return_type : Type[T], eager = True, scope = "singleton"):
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.dependencies:
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": lambda feature : environment.has_feature(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 isinstance(existing_provider, AmbiguousProvider):
712
- cast(AmbiguousProvider, existing_provider).add_provider(provider)
713
- else:
714
- cache[type] = AmbiguousProvider(type, existing_provider, provider)
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.method, return_type, create_decorator.args[0],
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
- @environment()
943
- class SimpleEnvironment:
982
+ @module()
983
+ class Module:
944
984
  def __init__(self):
945
985
  pass
946
986
 
947
- environment = Environment(SimpleEnvironment)
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[module_name]
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
- return module.__package__
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
 
@@ -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:
@@ -1,7 +1,8 @@
1
1
  """
2
2
  Dynamic proxies for method interception and delegation.
3
3
  """
4
- from typing import Generic, TypeVar, Type
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
- def __init__(self, type: Type[T], name: str, *args, **kwargs):
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.name = name
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
- def wrapper(*args, **kwargs):
57
- return self.invocation_handler.invoke(DynamicProxy.Invocation(self.type, name, *args, **kwargs))
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
- return wrapper
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, getmembers
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]: ths list
80
+ list[DecoratorDescriptor]: the list
71
81
  """
72
- return getattr(func_or_class, '__decorators__', [])
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, value)
217
- for name, value in getmembers(cls, predicate=inspect.isfunction)
218
- if name in cls.__dict__
240
+ (name, attr)
241
+ for name, attr in cls.__dict__.items()
242
+ if isinstance(attr, FunctionType)
219
243
  ]
220
244
 
221
245
  # public