wireup 2.2.1__tar.gz → 2.2.2__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.
Files changed (29) hide show
  1. {wireup-2.2.1 → wireup-2.2.2}/PKG-INFO +5 -4
  2. {wireup-2.2.1 → wireup-2.2.2}/pyproject.toml +3 -2
  3. {wireup-2.2.1 → wireup-2.2.2}/wireup/__init__.py +2 -1
  4. {wireup-2.2.1 → wireup-2.2.2}/wireup/_annotations.py +9 -3
  5. {wireup-2.2.1 → wireup-2.2.2}/wireup/integration/django/apps.py +1 -1
  6. {wireup-2.2.1 → wireup-2.2.2}/wireup/ioc/container/__init__.py +8 -1
  7. {wireup-2.2.1 → wireup-2.2.2}/wireup/ioc/service_registry.py +2 -2
  8. {wireup-2.2.1 → wireup-2.2.2}/wireup/ioc/util.py +71 -19
  9. {wireup-2.2.1 → wireup-2.2.2}/readme.md +0 -0
  10. {wireup-2.2.1 → wireup-2.2.2}/wireup/_decorators.py +0 -0
  11. {wireup-2.2.1 → wireup-2.2.2}/wireup/_discovery.py +0 -0
  12. {wireup-2.2.1 → wireup-2.2.2}/wireup/errors.py +0 -0
  13. {wireup-2.2.1 → wireup-2.2.2}/wireup/integration/__init__.py +0 -0
  14. {wireup-2.2.1 → wireup-2.2.2}/wireup/integration/aiohttp.py +0 -0
  15. {wireup-2.2.1 → wireup-2.2.2}/wireup/integration/click.py +0 -0
  16. {wireup-2.2.1 → wireup-2.2.2}/wireup/integration/django/__init__.py +0 -0
  17. {wireup-2.2.1 → wireup-2.2.2}/wireup/integration/fastapi.py +0 -0
  18. {wireup-2.2.1 → wireup-2.2.2}/wireup/integration/flask.py +0 -0
  19. {wireup-2.2.1 → wireup-2.2.2}/wireup/integration/starlette.py +0 -0
  20. {wireup-2.2.1 → wireup-2.2.2}/wireup/ioc/__init__.py +0 -0
  21. {wireup-2.2.1 → wireup-2.2.2}/wireup/ioc/_exit_stack.py +0 -0
  22. {wireup-2.2.1 → wireup-2.2.2}/wireup/ioc/container/async_container.py +0 -0
  23. {wireup-2.2.1 → wireup-2.2.2}/wireup/ioc/container/base_container.py +0 -0
  24. {wireup-2.2.1 → wireup-2.2.2}/wireup/ioc/container/sync_container.py +0 -0
  25. {wireup-2.2.1 → wireup-2.2.2}/wireup/ioc/factory_compiler.py +0 -0
  26. {wireup-2.2.1 → wireup-2.2.2}/wireup/ioc/override_manager.py +0 -0
  27. {wireup-2.2.1 → wireup-2.2.2}/wireup/ioc/parameter.py +0 -0
  28. {wireup-2.2.1 → wireup-2.2.2}/wireup/ioc/types.py +0 -0
  29. {wireup-2.2.1 → wireup-2.2.2}/wireup/py.typed +0 -0
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: wireup
3
- Version: 2.2.1
3
+ Version: 2.2.2
4
4
  Summary: Python Dependency Injection Library
5
- Home-page: https://github.com/maldoinc/wireup
6
5
  License: MIT
7
6
  Keywords: flask,django,injector,dependency injection,dependency injection container,dependency injector
8
7
  Author: Aldo Mateli
@@ -26,14 +25,16 @@ Classifier: Programming Language :: Python :: 3.10
26
25
  Classifier: Programming Language :: Python :: 3.11
27
26
  Classifier: Programming Language :: Python :: 3.12
28
27
  Classifier: Programming Language :: Python :: 3.13
28
+ Classifier: Programming Language :: Python :: 3.14
29
29
  Classifier: Programming Language :: Python :: 3 :: Only
30
30
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
31
31
  Classifier: Typing :: Typed
32
32
  Provides-Extra: eval-type
33
- Requires-Dist: eval-type-backport (>=0.2.0,<0.3.0) ; extra == "eval-type"
33
+ Requires-Dist: eval-type-backport (>=0.2.0,<0.3.0) ; (python_version < "3.11") and (extra == "eval-type")
34
34
  Requires-Dist: typing_extensions (>=4.7,<5.0)
35
35
  Project-URL: Changelog, https://github.com/maldoinc/wireup/releases
36
36
  Project-URL: Documentation, https://maldoinc.github.io/wireup/
37
+ Project-URL: Homepage, https://github.com/maldoinc/wireup
37
38
  Project-URL: Repository, https://github.com/maldoinc/wireup
38
39
  Description-Content-Type: text/markdown
39
40
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "wireup"
3
- version = "2.2.1"
3
+ version = "2.2.2"
4
4
  description = "Python Dependency Injection Library"
5
5
  authors = ["Aldo Mateli <aldo.mateli@gmail.com>"]
6
6
  license = "MIT"
@@ -33,6 +33,7 @@ classifiers = [
33
33
  "Programming Language :: Python :: 3.11",
34
34
  "Programming Language :: Python :: 3.12",
35
35
  "Programming Language :: Python :: 3.13",
36
+ "Programming Language :: Python :: 3.14",
36
37
  "Programming Language :: Python :: 3 :: Only",
37
38
  "Typing :: Typed",
38
39
  "Topic :: Software Development :: Libraries :: Python Modules",
@@ -46,7 +47,7 @@ Changelog = "https://github.com/maldoinc/wireup/releases"
46
47
  [tool.poetry.dependencies]
47
48
  python = "^3.8"
48
49
  typing_extensions = "^4.7"
49
- eval-type-backport = { version = "^0.2.0", optional = true }
50
+ eval-type-backport = { version = "^0.2.0", optional = true, python = "<3.11" }
50
51
 
51
52
  [tool.poetry.extras]
52
53
  eval-type = ["eval-type-backport"]
@@ -1,4 +1,4 @@
1
- from wireup._annotations import Inject, Injected, abstract, service
1
+ from wireup._annotations import Inject, Injected, abstract, injectable, service
2
2
  from wireup._decorators import inject_from_container
3
3
  from wireup.ioc.container import (
4
4
  create_async_container,
@@ -21,5 +21,6 @@ __all__ = [
21
21
  "create_async_container",
22
22
  "create_sync_container",
23
23
  "inject_from_container",
24
+ "injectable",
24
25
  "service",
25
26
  ]
@@ -23,6 +23,7 @@ if TYPE_CHECKING:
23
23
 
24
24
  def Inject( # noqa: N802
25
25
  *,
26
+ config: str | None = None,
26
27
  param: str | None = None,
27
28
  expr: str | None = None,
28
29
  qualifier: Qualifier | None = None,
@@ -41,6 +42,8 @@ def Inject( # noqa: N802
41
42
  """
42
43
  res: InjectableType
43
44
 
45
+ if config:
46
+ res = ParameterWrapper(config)
44
47
  if param:
45
48
  res = ParameterWrapper(param)
46
49
  elif expr:
@@ -89,7 +92,7 @@ class AbstractDeclaration:
89
92
 
90
93
 
91
94
  @overload
92
- def service(
95
+ def injectable(
93
96
  obj: None = None,
94
97
  *,
95
98
  qualifier: Qualifier | None = None,
@@ -99,7 +102,7 @@ def service(
99
102
 
100
103
 
101
104
  @overload
102
- def service(
105
+ def injectable(
103
106
  obj: T,
104
107
  *,
105
108
  qualifier: Qualifier | None = None,
@@ -108,7 +111,7 @@ def service(
108
111
  pass
109
112
 
110
113
 
111
- def service(
114
+ def injectable(
112
115
  obj: T | None = None,
113
116
  *,
114
117
  qualifier: Qualifier | None = None,
@@ -126,6 +129,9 @@ def service(
126
129
  return _service_decorator if obj is None else _service_decorator(obj)
127
130
 
128
131
 
132
+ service = injectable
133
+
134
+
129
135
  def abstract(cls: type[T]) -> type[T]:
130
136
  """Mark the decorated class as an abstract service."""
131
137
  cls.__wireup_registration__ = AbstractDeclaration(cls) # type: ignore[attr-defined]
@@ -137,7 +137,7 @@ class WireupConfig(AppConfig):
137
137
  injected_names = {
138
138
  name: self.container.params.get(param.annotation.param)
139
139
  if isinstance(param.annotation, ParameterWrapper)
140
- else get_request_container().get(param.klass, qualifier=param.qualifier_value)
140
+ else get_request_container()._synchronous_get(param.klass, qualifier=param.qualifier_value)
141
141
  for name, param in names_to_inject.items()
142
142
  if param.annotation
143
143
  }
@@ -92,7 +92,9 @@ def _merge_definitions(
92
92
  def create_sync_container(
93
93
  service_modules: list[ModuleType] | None = None,
94
94
  services: list[Any] | None = None,
95
+ injectables: list[Any] | None = None,
95
96
  parameters: dict[str, Any] | None = None,
97
+ config: dict[str, Any] | None = None,
96
98
  ) -> SyncContainer:
97
99
  """Create a Wireup container.
98
100
 
@@ -104,7 +106,12 @@ def create_sync_container(
104
106
  request parameters via the `Inject(param="name")` syntax.
105
107
  :raises WireupError: Raised if the dependencies cannot be fully resolved.
106
108
  """
107
- return _create_container(SyncContainer, service_modules=service_modules, services=services, parameters=parameters)
109
+ return _create_container(
110
+ SyncContainer,
111
+ service_modules=service_modules,
112
+ services=[*(services or []), *(injectables or [])],
113
+ parameters=parameters,
114
+ )
108
115
 
109
116
 
110
117
  def create_async_container(
@@ -80,7 +80,7 @@ def _function_get_unwrapped_return_type(fn: Callable[..., T]) -> type[T] | None:
80
80
  return fn
81
81
 
82
82
  if ret := fn.__annotations__.get("return"):
83
- ret = ensure_is_type(ret, globalns=get_globals(fn))
83
+ ret = ensure_is_type(ret, globalns_supplier=lambda: get_globals(fn))
84
84
  if not ret:
85
85
  return None
86
86
 
@@ -225,7 +225,7 @@ class ServiceRegistry:
225
225
  ) -> None:
226
226
  """Init and collect all the necessary dependencies to initialize the specified target."""
227
227
  for name, parameter in inspect.signature(target).parameters.items():
228
- annotated_param = param_get_annotation(parameter, globalns=get_globals(target))
228
+ annotated_param = param_get_annotation(parameter, globalns_supplier=lambda: get_globals(target))
229
229
 
230
230
  if not annotated_param:
231
231
  msg = f"Wireup dependencies must have types. Please add a type to the '{name}' parameter in {target}."
@@ -3,15 +3,18 @@ from __future__ import annotations
3
3
  import functools
4
4
  import importlib
5
5
  import inspect
6
+ import sys
6
7
  import types
7
8
  import typing
8
9
  from inspect import Parameter
9
- from typing import Any, Sequence, TypeVar
10
+ from typing import Any, Sequence, TypeVar, cast
10
11
 
11
12
  from wireup.errors import WireupError
12
13
  from wireup.ioc.types import AnnotatedParameter, AnyCallable, InjectableType
13
14
 
15
+ T = TypeVar("T")
14
16
  _OPTIONAL_UNION_ARG_COUNT = 2
17
+ _eval_type = cast("Callable[..., Any]", typing._eval_type) # type: ignore[attr-defined]
15
18
 
16
19
  if typing.TYPE_CHECKING:
17
20
  from collections.abc import Callable
@@ -43,14 +46,18 @@ def _get_wireup_annotation(metadata: Sequence[Any]) -> InjectableType | None:
43
46
  return annotations[0]
44
47
 
45
48
 
46
- def param_get_annotation(parameter: Parameter, *, globalns: dict[str, Any]) -> AnnotatedParameter | None:
49
+ def param_get_annotation(
50
+ parameter: Parameter,
51
+ *,
52
+ globalns_supplier: Callable[[], dict[str, Any]],
53
+ ) -> AnnotatedParameter | None:
47
54
  """Get the annotation injection type from a signature's Parameter.
48
55
 
49
56
  Returns the first injectable annotation for an Annotated type or the default value.
50
57
  Also handles Optional types by marking them as optional in the AnnotatedParameter.
51
58
  Supports both Annotated[Optional[T], ...] and Optional[Annotated[T, ...]] patterns.
52
59
  """
53
- resolved_type: type[Any] | None = ensure_is_type(parameter.annotation, globalns=globalns)
60
+ resolved_type: type[Any] | None = ensure_is_type(parameter.annotation, globalns_supplier=globalns_supplier)
54
61
 
55
62
  if resolved_type is Parameter.empty:
56
63
  resolved_type = None
@@ -78,10 +85,24 @@ def param_get_annotation(parameter: Parameter, *, globalns: dict[str, Any]) -> A
78
85
  return AnnotatedParameter(klass=inner_type, annotation=annotation)
79
86
 
80
87
 
88
+ def _type_get_globals(typ: type) -> dict[str, Any]:
89
+ """
90
+ Merge globals from parent classes with the child class's module globals,
91
+ with child class globals taking precedence.
92
+ """
93
+ merged_globals: dict[str, Any] = {}
94
+
95
+ for base_class in reversed(typ.__mro__[:-1]):
96
+ base_globals = importlib.import_module(base_class.__module__).__dict__
97
+ merged_globals.update(base_globals)
98
+
99
+ return merged_globals
100
+
101
+
81
102
  def get_globals(obj: type[Any] | Callable[..., Any]) -> dict[str, Any]:
82
103
  """Return the globals for the given object."""
83
104
  if isinstance(obj, type):
84
- return importlib.import_module(obj.__module__).__dict__
105
+ return _type_get_globals(obj)
85
106
 
86
107
  # Unwrap nested functools.partial to get the underlying function
87
108
  while isinstance(obj, functools.partial):
@@ -90,31 +111,62 @@ def get_globals(obj: type[Any] | Callable[..., Any]) -> dict[str, Any]:
90
111
  return obj.__globals__
91
112
 
92
113
 
93
- T = TypeVar("T")
114
+ def _eval_type_native(
115
+ value: typing.ForwardRef,
116
+ globalns: dict[str, Any] | None = None,
117
+ ) -> Any:
118
+ """Evaluate a ForwardRef using the native typing._eval_type function.
119
+
120
+ This function handles version-specific differences in typing._eval_type.
121
+ """
122
+ if sys.version_info >= (3, 14):
123
+ res = _eval_type(value, globalns, None, type_params=())
124
+
125
+ return type(None) if res is None else res
126
+
127
+ return _eval_type(value, globalns, None)
94
128
 
95
129
 
96
- def ensure_is_type(value: type[T] | str, globalns: dict[str, Any] | None = None) -> type[T] | None:
130
+ def ensure_is_type(value: type[T] | str, globalns_supplier: Callable[[], dict[str, Any]]) -> type[T] | None:
97
131
  """Ensure the given value represents a type.
98
132
 
99
- If it is a string it will be evaluated using eval_type_backport.
133
+ If it is a string it will be evaluated, first trying the native typing._eval_type,
134
+ and falling back to eval_type_backport if needed.
135
+
136
+ This approach ensures compatibility with Python 3.14+ where eval_type_backport
137
+ cannot be imported due to ForwardRef subclassing restrictions.
100
138
  """
101
- if isinstance(value, str):
139
+ if not isinstance(value, str):
140
+ return value
141
+
142
+ forward_ref = typing.ForwardRef(value)
143
+
144
+ try:
145
+ # First, try using the native typing._eval_type then fall back to eval_type_backport.
146
+ # For a more complete solution see the pydantic implementation below.
147
+ # See: https://github.com/pydantic/pydantic/blob/f42171c760d43b9522fde513ae6e209790f7fefb/pydantic/_internal/_typing_extra.py#L485-L512
148
+ return _eval_type_native(forward_ref, globalns=globalns_supplier()) # type:ignore[no-any-return]
149
+ except TypeError as eval_type_error:
102
150
  try:
103
151
  import eval_type_backport
104
-
105
- return eval_type_backport.eval_type_backport( # type:ignore[no-any-return]
106
- eval_type_backport.ForwardRef(value), globalns=globalns, try_default=False
107
- )
108
- except NameError:
109
- return None
110
- except ImportError as e:
152
+ except ImportError as import_error:
111
153
  msg = (
112
- "Using __future__ annotations in Wireup requires the eval_type_backport package to be installed. "
154
+ f"Error evaluating type annotation '{value}'. "
155
+ f"Got: TypeError: {eval_type_error}.\n"
156
+ "Tip: Try using the eval_type_backport package to resolve stringified types.\n"
113
157
  "See: https://maldoinc.github.io/wireup/latest/future_annotations/"
114
158
  )
115
- raise WireupError(msg) from e
159
+ raise WireupError(msg) from import_error
160
+
161
+ return eval_type_backport.eval_type_backport(forward_ref, globalns=globalns_supplier(), try_default=False) # type:ignore[no-any-return]
116
162
 
117
- return value
163
+ except NameError as e:
164
+ msg = (
165
+ f"Error evaluating type annotation '{value}'. Got NameError: {e!s}. "
166
+ "Make sure the type is correctly imported and not inside a TYPE_CHECKING block.\n"
167
+ "See: https://maldoinc.github.io/wireup/latest/future_annotations/"
168
+ )
169
+ raise WireupError(msg) from e
118
170
 
119
171
 
120
172
  def unwrap_optional_type(type_: Any) -> Any:
@@ -182,7 +234,7 @@ def get_inject_annotated_parameters(target: AnyCallable) -> dict[str, AnnotatedP
182
234
  return {
183
235
  name: param
184
236
  for name, parameter in inspect.signature(target).parameters.items()
185
- if (param := param_get_annotation(parameter, globalns=get_globals(target)))
237
+ if (param := param_get_annotation(parameter, globalns_supplier=lambda: get_globals(target)))
186
238
  and isinstance(param.annotation, InjectableType)
187
239
  }
188
240
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes