omlish 0.0.0.dev5__py3-none-any.whl → 0.0.0.dev6__py3-none-any.whl

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 omlish might be problematic. Click here for more details.

Files changed (100) hide show
  1. omlish/__about__.py +1 -1
  2. omlish/asyncs/__init__.py +9 -0
  3. omlish/asyncs/anyio.py +83 -19
  4. omlish/asyncs/asyncio.py +23 -0
  5. omlish/asyncs/asyncs.py +9 -6
  6. omlish/asyncs/bridge.py +316 -0
  7. omlish/asyncs/trio_asyncio.py +7 -3
  8. omlish/collections/__init__.py +1 -0
  9. omlish/collections/identity.py +7 -0
  10. omlish/configs/strings.py +94 -0
  11. omlish/dataclasses/__init__.py +9 -0
  12. omlish/dataclasses/impl/copy.py +30 -0
  13. omlish/dataclasses/impl/exceptions.py +6 -0
  14. omlish/dataclasses/impl/fields.py +24 -25
  15. omlish/dataclasses/impl/init.py +4 -2
  16. omlish/dataclasses/impl/main.py +2 -0
  17. omlish/dataclasses/utils.py +44 -0
  18. omlish/diag/__init__.py +4 -0
  19. omlish/diag/procfs.py +2 -2
  20. omlish/{testing → diag}/pydevd.py +35 -0
  21. omlish/dispatch/_dispatch2.py +65 -0
  22. omlish/dispatch/_dispatch3.py +104 -0
  23. omlish/docker.py +1 -1
  24. omlish/fnpairs.py +11 -0
  25. omlish/http/asgi.py +2 -1
  26. omlish/http/collections.py +15 -0
  27. omlish/http/consts.py +16 -1
  28. omlish/http/sessions.py +10 -3
  29. omlish/inject/__init__.py +45 -17
  30. omlish/inject/binder.py +185 -5
  31. omlish/inject/bindings.py +3 -36
  32. omlish/inject/eagers.py +2 -8
  33. omlish/inject/elements.py +30 -9
  34. omlish/inject/exceptions.py +1 -1
  35. omlish/inject/impl/elements.py +37 -12
  36. omlish/inject/impl/injector.py +19 -2
  37. omlish/inject/impl/inspect.py +33 -5
  38. omlish/inject/impl/origins.py +75 -0
  39. omlish/inject/impl/{private.py → privates.py} +2 -2
  40. omlish/inject/impl/scopes.py +6 -2
  41. omlish/inject/injector.py +8 -4
  42. omlish/inject/inspect.py +18 -0
  43. omlish/inject/keys.py +8 -14
  44. omlish/inject/listeners.py +26 -0
  45. omlish/inject/managed.py +76 -10
  46. omlish/inject/multis.py +68 -18
  47. omlish/inject/origins.py +27 -0
  48. omlish/inject/overrides.py +5 -4
  49. omlish/inject/{private.py → privates.py} +6 -10
  50. omlish/inject/providers.py +12 -85
  51. omlish/inject/scopes.py +13 -6
  52. omlish/inject/types.py +3 -1
  53. omlish/lang/__init__.py +8 -2
  54. omlish/lang/cached.py +2 -2
  55. omlish/lang/classes/restrict.py +2 -1
  56. omlish/lang/classes/simple.py +18 -8
  57. omlish/lang/contextmanagers.py +12 -3
  58. omlish/lang/descriptors.py +131 -0
  59. omlish/lang/functions.py +8 -28
  60. omlish/lang/iterables.py +20 -1
  61. omlish/lang/typing.py +5 -0
  62. omlish/lifecycles/__init__.py +34 -0
  63. omlish/lifecycles/abstract.py +43 -0
  64. omlish/lifecycles/base.py +51 -0
  65. omlish/lifecycles/contextmanagers.py +74 -0
  66. omlish/lifecycles/controller.py +116 -0
  67. omlish/lifecycles/manager.py +161 -0
  68. omlish/lifecycles/states.py +43 -0
  69. omlish/lifecycles/transitions.py +64 -0
  70. omlish/logs/formatters.py +1 -1
  71. omlish/marshal/__init__.py +4 -0
  72. omlish/marshal/naming.py +4 -0
  73. omlish/marshal/objects.py +1 -0
  74. omlish/marshal/polymorphism.py +4 -4
  75. omlish/reflect.py +134 -19
  76. omlish/secrets/__init__.py +7 -0
  77. omlish/secrets/marshal.py +41 -0
  78. omlish/secrets/passwords.py +120 -0
  79. omlish/secrets/secrets.py +47 -0
  80. omlish/serde/__init__.py +0 -0
  81. omlish/{configs → serde}/dotenv.py +12 -24
  82. omlish/{json.py → serde/json.py} +2 -1
  83. omlish/serde/yaml.py +223 -0
  84. omlish/sql/dbs.py +1 -1
  85. omlish/sql/duckdb.py +136 -0
  86. omlish/sql/sqlean.py +17 -0
  87. omlish/term.py +1 -1
  88. omlish/testing/pytest/__init__.py +3 -2
  89. omlish/testing/pytest/inject/harness.py +3 -3
  90. omlish/testing/pytest/marks.py +4 -7
  91. omlish/testing/pytest/plugins/__init__.py +1 -0
  92. omlish/testing/pytest/plugins/asyncs.py +136 -0
  93. omlish/testing/pytest/plugins/pydevd.py +1 -1
  94. omlish/text/glyphsplit.py +92 -0
  95. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/METADATA +1 -1
  96. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/RECORD +100 -72
  97. /omlish/{configs → serde}/props.py +0 -0
  98. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/LICENSE +0 -0
  99. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/WHEEL +0 -0
  100. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/top_level.txt +0 -0
omlish/inject/__init__.py CHANGED
@@ -1,26 +1,30 @@
1
1
  """
2
2
  ~> https://github.com/google/guice/commit/70248eafa90cd70a68b293763e53f6aec656e73c
3
3
  """
4
+ from .binder import ( # noqa
5
+ bind,
6
+ bind_as_fn,
7
+ )
8
+
4
9
  from .bindings import ( # noqa
5
10
  Binding,
6
- as_,
7
- as_binding,
8
11
  )
9
12
 
10
13
  from .eagers import ( # noqa
11
- eager,
14
+ Eager,
12
15
  )
13
16
 
14
17
  from .elements import ( # noqa
15
- as_elements,
16
18
  Element,
19
+ Elemental,
17
20
  Elements,
21
+ as_elements,
18
22
  )
19
23
 
20
24
  from .exceptions import ( # noqa
21
25
  BaseKeyError,
26
+ ConflictingKeyError,
22
27
  CyclicDependencyError,
23
- DuplicateKeyError,
24
28
  ScopeAlreadyOpenError,
25
29
  ScopeError,
26
30
  ScopeNotOpenError,
@@ -35,57 +39,81 @@ from .injector import ( # noqa
35
39
  from .inspect import ( # noqa
36
40
  Kwarg,
37
41
  KwargsTarget,
42
+ build_kwargs_target,
43
+ tag,
38
44
  )
39
45
 
40
46
  from .keys import ( # noqa
41
47
  Key,
42
48
  as_key,
43
- tag,
49
+ )
50
+
51
+ from .listeners import ( # noqa
52
+ ProvisionListener,
53
+ ProvisionListenerBinding,
54
+ bind_provision_listener,
44
55
  )
45
56
 
46
57
  from .managed import ( # noqa
58
+ create_async_managed_injector,
47
59
  create_managed_injector,
60
+ make_async_managed_provider,
61
+ make_managed_provider,
48
62
  )
49
63
 
50
64
  from .multis import ( # noqa
51
65
  MapBinding,
66
+ MapProvider,
52
67
  SetBinding,
53
- bind_map_provider,
54
- bind_set_provider,
68
+ SetProvider,
69
+ MapBinder,
70
+ SetBinder,
71
+
72
+ MapBinder as map_binder, # noqa
73
+ SetBinder as set_binder, # noqa
55
74
  )
56
75
 
57
76
 
58
77
  from .overrides import ( # noqa
78
+ Overrides,
59
79
  override,
60
80
  )
61
81
 
62
- from .private import ( # noqa
63
- expose,
82
+ from .origins import ( # noqa
83
+ HasOrigins,
84
+ Origin,
85
+ Origins,
86
+ )
87
+
88
+ from .privates import ( # noqa
89
+ Expose,
90
+ Private,
64
91
  private,
92
+
93
+ Expose as expose, # noqa
65
94
  )
66
95
 
67
96
  from .providers import ( # noqa
97
+ ConstProvider,
98
+ CtorProvider,
99
+ FnProvider,
100
+ LinkProvider,
68
101
  Provider,
69
- as_provider,
70
- const,
71
- ctor,
72
- fn,
73
- link,
74
102
  )
75
103
 
76
104
  from .scopes import ( # noqa
77
105
  ScopeBinding,
106
+ ScopeSeededProvider,
78
107
  SeededScope,
79
108
  Singleton,
80
109
  Thread,
81
110
  bind_scope,
82
111
  bind_scope_seed,
83
112
  enter_seeded_scope,
84
- in_,
85
- singleton,
86
113
  )
87
114
 
88
115
  from .types import ( # noqa
89
116
  Scope,
117
+ Tag,
90
118
  Unscoped,
91
119
  )
omlish/inject/binder.py CHANGED
@@ -1,12 +1,192 @@
1
+ """
2
+ TODO:
3
+ - multis?
4
+
5
+ class SetBinding(Element, lang.Final):
6
+ class SetProvider(Provider):
7
+
8
+ class MapBinding(Element, lang.Final):
9
+ class MapProvider(Provider):
10
+ """
11
+ import functools
12
+ import inspect
13
+ import types
1
14
  import typing as ta
2
15
 
16
+ from .. import check
17
+ from .. import dataclasses as dc
18
+ from .. import lang
19
+ from .. import reflect as rfl
20
+ from .bindings import Binding
21
+ from .eagers import Eager
3
22
  from .elements import Element
4
23
  from .elements import Elements
5
- from .elements import as_elements
24
+ from .keys import Key
25
+ from .keys import as_key
26
+ from .privates import Expose
27
+ from .providers import ConstProvider
28
+ from .providers import CtorProvider
29
+ from .providers import FnProvider
30
+ from .providers import LinkProvider
31
+ from .providers import Provider
32
+ from .scopes import SCOPE_ALIASES
33
+ from .scopes import Singleton
34
+ from .types import Scope
35
+ from .types import Unscoped
36
+
37
+
38
+ if ta.TYPE_CHECKING:
39
+ from .impl.inspect import inspect as _inspect
40
+ else:
41
+ _inspect = lang.proxy_import('.impl.inspect', __package__)
42
+
43
+
44
+ T = ta.TypeVar('T')
45
+
46
+
47
+ ##
48
+
49
+
50
+ _FN_TYPES: tuple[type, ...] = (
51
+ types.FunctionType,
52
+ types.MethodType,
53
+
54
+ classmethod,
55
+ staticmethod,
56
+
57
+ functools.partial,
58
+ functools.partialmethod,
59
+ )
60
+
61
+
62
+ def _is_fn(obj: ta.Any) -> bool:
63
+ return isinstance(obj, _FN_TYPES)
64
+
65
+
66
+ def bind_as_fn(cls: type[T]) -> type[T]:
67
+ check.isinstance(cls, type)
68
+ global _FN_TYPES
69
+ if cls not in _FN_TYPES:
70
+ _FN_TYPES = (*_FN_TYPES, cls)
71
+ return cls
72
+
73
+
74
+ ##
75
+
76
+
77
+ _BANNED_BIND_TYPES = (
78
+ Element,
79
+ Provider,
80
+ Elements,
81
+ Scope,
82
+ )
83
+
84
+
85
+ def bind(
86
+ obj: ta.Any,
87
+ *,
88
+ tag: ta.Any = None,
89
+
90
+ in_: Scope | None = None,
91
+ singleton: bool = False,
92
+
93
+ to_fn: ta.Any = None,
94
+ to_ctor: ta.Any = None,
95
+ to_const: ta.Any = None,
96
+ to_key: ta.Any = None,
97
+
98
+ eager: bool = False,
99
+ expose: bool = False,
100
+ ) -> Element | Elements:
101
+ if obj is None or obj is inspect.Parameter.empty:
102
+ raise TypeError(obj)
103
+ if isinstance(obj, _BANNED_BIND_TYPES):
104
+ raise TypeError(obj)
105
+
106
+ ##
107
+
108
+ has_to = (
109
+ to_fn is not None or
110
+ to_ctor is not None or
111
+ to_const is not None or
112
+ to_key is not None
113
+ )
114
+ if isinstance(obj, Key):
115
+ key = obj
116
+ elif isinstance(obj, type):
117
+ if not has_to:
118
+ to_ctor = obj
119
+ key = Key(obj)
120
+ elif isinstance(obj, rfl.TYPES) or rfl.is_type(obj):
121
+ key = Key(obj)
122
+ elif _is_fn(obj) and not has_to:
123
+ sig = _inspect.signature(obj)
124
+ ty = rfl.type_(sig.return_annotation)
125
+ to_fn = obj
126
+ key = Key(ty)
127
+ else:
128
+ if to_const is not None:
129
+ raise TypeError('Cannot bind instance with to_const')
130
+ to_const = obj
131
+ key = Key(type(obj))
132
+ del has_to
133
+
134
+ ##
135
+
136
+ if tag is not None:
137
+ if key.tag is not None:
138
+ raise TypeError('Tag already set')
139
+ key = dc.replace(key, tag=tag)
140
+
141
+ ##
142
+
143
+ providers: list[Provider] = []
144
+ if to_fn is not None:
145
+ providers.append(FnProvider(to_fn))
146
+ if to_ctor is not None:
147
+ providers.append(CtorProvider(to_ctor))
148
+ if to_const is not None:
149
+ providers.append(ConstProvider(to_const))
150
+ if to_key is not None:
151
+ providers.append(LinkProvider(as_key(to_key)))
152
+ if not providers:
153
+ raise TypeError('Must specify provider')
154
+ if len(providers) > 1:
155
+ raise TypeError('May not specify multiple providers')
156
+ provider, = providers
157
+
158
+ ##
159
+
160
+ scopes: list[Scope] = []
161
+ if in_ is not None:
162
+ if isinstance(in_, str):
163
+ scopes.append(SCOPE_ALIASES[in_])
164
+ else:
165
+ scopes.append(check.isinstance(in_, Scope))
166
+ if singleton:
167
+ scopes.append(Singleton())
168
+ if len(scopes) > 1:
169
+ raise TypeError('May not specify multiple scopes')
170
+ scope: Scope
171
+ if not scopes:
172
+ scope = Unscoped()
173
+ else:
174
+ scope, = scopes
175
+
176
+ ##
177
+
178
+ binding = Binding(key, provider, scope)
179
+
180
+ ##
6
181
 
182
+ elements: list[Element] = [binding]
7
183
 
8
- def bind(*args: ta.Any) -> Elements:
9
- if all(isinstance(a, (Element, Elements)) for a in args):
10
- return as_elements(*args)
184
+ if eager:
185
+ elements.append(Eager(key))
186
+ if expose:
187
+ elements.append(Expose(key))
11
188
 
12
- raise TypeError(args)
189
+ if len(elements) == 1:
190
+ return elements[0]
191
+ else:
192
+ return Elements(frozenset(elements))
omlish/inject/bindings.py CHANGED
@@ -1,49 +1,16 @@
1
- import typing as ta
2
-
3
1
  from .. import check
4
2
  from .. import dataclasses as dc
5
3
  from .. import lang
6
4
  from .elements import Element
7
- from .elements import Elements
8
5
  from .keys import Key
9
- from .keys import as_key
10
6
  from .providers import Provider
11
- from .providers import as_provider
12
- from .providers import const
13
- from .providers import ctor
14
- from .providers import fn
15
7
  from .types import Scope
16
8
  from .types import Unscoped
17
9
 
18
10
 
19
- ##
20
-
21
-
22
11
  @dc.dataclass(frozen=True)
23
12
  @dc.extra_params(cache_hash=True)
24
13
  class Binding(Element, lang.Final):
25
- key: Key
26
- provider: Provider
27
- scope: Scope = Unscoped()
28
-
29
-
30
- ##
31
-
32
-
33
- def as_binding(o: ta.Any) -> Binding:
34
- check.not_none(o)
35
- if isinstance(o, Binding):
36
- return o
37
- check.not_isinstance(o, (Element, Elements))
38
- if isinstance(o, Provider):
39
- return Binding(Key(check.not_none(o.provided_ty())), o)
40
- if isinstance(o, type):
41
- return as_binding(ctor(o))
42
- if callable(o):
43
- return as_binding(fn(o))
44
- ty = type(o)
45
- return Binding(Key(ty), const(o, ty))
46
-
47
-
48
- def as_(k: ta.Any, p: ta.Any) -> Binding:
49
- return Binding(as_key(k), as_provider(p))
14
+ key: Key = dc.xfield(coerce=check.of_isinstance(Key))
15
+ provider: Provider = dc.xfield(coerce=check.of_isinstance(Provider))
16
+ scope: Scope = dc.xfield(default=Unscoped(), coerce=check.of_isinstance(Scope))
omlish/inject/eagers.py CHANGED
@@ -2,20 +2,14 @@
2
2
  TODO:
3
3
  - SCOPED - eagers for EACH SCOPE
4
4
  """
5
- import typing as ta
6
-
5
+ from .. import check
7
6
  from .. import dataclasses as dc
8
7
  from .. import lang
9
8
  from .elements import Element
10
9
  from .keys import Key
11
- from .keys import as_key
12
10
 
13
11
 
14
12
  @dc.dataclass(frozen=True)
15
13
  @dc.extra_params(cache_hash=True)
16
14
  class Eager(Element, lang.Final):
17
- key: Key
18
-
19
-
20
- def eager(k: ta.Any) -> Element:
21
- return Eager(as_key(k))
15
+ key: Key = dc.xfield(coerce=check.of_isinstance(Key))
omlish/inject/elements.py CHANGED
@@ -1,21 +1,26 @@
1
- """
2
- TODO:
3
- - as_element[s] - universal
4
- """
1
+ import abc
5
2
  import typing as ta
6
3
 
4
+ from .. import check
7
5
  from .. import dataclasses as dc
8
6
  from .. import lang
7
+ from .impl.origins import HasOriginsImpl
9
8
 
10
9
 
11
- class Element(lang.Abstract):
10
+ class Element(HasOriginsImpl, lang.Abstract, lang.PackageSealed):
12
11
  pass
13
12
 
14
13
 
14
+ class ElementGenerator(lang.Abstract, lang.PackageSealed):
15
+ @abc.abstractmethod
16
+ def __iter__(self) -> ta.Iterator[Element]:
17
+ raise NotImplementedError
18
+
19
+
15
20
  @dc.dataclass(frozen=True)
16
21
  class Elements(lang.Final):
17
- es: frozenset[Element] | None = None
18
- cs: frozenset['Elements'] | None = None
22
+ es: frozenset[Element] | None = dc.xfield(None, coerce=check.of_isinstance((frozenset, None)))
23
+ cs: frozenset['Elements'] | None = dc.xfield(None, coerce=check.of_isinstance((frozenset, None)))
19
24
 
20
25
  def __iter__(self) -> ta.Generator[Element, None, None]:
21
26
  if self.es:
@@ -25,18 +30,34 @@ class Elements(lang.Final):
25
30
  yield from c
26
31
 
27
32
 
28
- def as_elements(*args: Element | Elements) -> Elements:
33
+ Elemental = ta.Union[ # noqa
34
+ Element,
35
+ Elements,
36
+ ElementGenerator,
37
+ ]
38
+
39
+
40
+ def as_elements(*args: Elemental) -> Elements:
29
41
  es: set[Element] = set()
30
42
  cs: set[Elements] = set()
31
- for a in args:
43
+
44
+ def rec(a):
32
45
  if isinstance(a, Element):
33
46
  es.add(a)
34
47
  elif isinstance(a, Elements):
35
48
  cs.add(a)
49
+ elif isinstance(a, ElementGenerator):
50
+ for n in a:
51
+ rec(n)
36
52
  else:
37
53
  raise TypeError(a)
54
+
55
+ for a in args:
56
+ rec(a)
57
+
38
58
  if not es and len(cs) == 1:
39
59
  return next(iter(cs))
60
+
40
61
  return Elements(
41
62
  frozenset(es) if es else None,
42
63
  frozenset(cs) if cs else None,
@@ -22,7 +22,7 @@ class UnboundKeyError(BaseKeyError):
22
22
 
23
23
 
24
24
  @dc.dataclass()
25
- class DuplicateKeyError(BaseKeyError):
25
+ class ConflictingKeyError(BaseKeyError):
26
26
  pass
27
27
 
28
28
 
@@ -10,6 +10,7 @@ Multi's + Scopes:
10
10
 
11
11
  Element Types:
12
12
  - Binding
13
+ - ProvisionListenerBinding
13
14
  - SetBinding
14
15
  - MapBinding
15
16
  - Eager
@@ -18,6 +19,7 @@ Element Types:
18
19
  - Private
19
20
  - ScopeBinding
20
21
  """
22
+ import copy
21
23
  import typing as ta
22
24
 
23
25
  from ... import check
@@ -27,28 +29,32 @@ from ..bindings import Binding
27
29
  from ..eagers import Eager
28
30
  from ..elements import Element
29
31
  from ..elements import Elements
30
- from ..exceptions import DuplicateKeyError
32
+ from ..exceptions import ConflictingKeyError
33
+ from ..exceptions import UnboundKeyError
31
34
  from ..keys import Key
35
+ from ..listeners import ProvisionListenerBinding
32
36
  from ..multis import MapBinding
33
37
  from ..multis import MapProvider
34
38
  from ..multis import SetBinding
35
39
  from ..multis import SetProvider
36
40
  from ..overrides import Overrides
37
- from ..private import Expose
38
- from ..private import Private
41
+ from ..privates import Expose
42
+ from ..privates import Private
39
43
  from ..scopes import ScopeBinding
40
44
  from ..types import Scope
41
45
  from .bindings import BindingImpl
42
46
  from .multis import make_multi_provider_impl
47
+ from .origins import Origins
48
+ from .origins import set_origins
43
49
  from .providers import ProviderImpl
44
50
  from .providers import make_provider_impl
45
51
  from .scopes import make_scope_impl
46
52
 
47
53
 
48
54
  if ta.TYPE_CHECKING:
49
- from . import private as private_
55
+ from . import privates as privates_
50
56
  else:
51
- private_ = lang.proxy_import('.private', __package__)
57
+ privates_ = lang.proxy_import('.privates', __package__)
52
58
 
53
59
 
54
60
  ElementT = ta.TypeVar('ElementT', bound=Element)
@@ -60,17 +66,17 @@ class ElementCollection(lang.Final):
60
66
 
61
67
  self._es = check.isinstance(es, Elements)
62
68
 
63
- self._private_infos: ta.MutableMapping[Private, private_.PrivateInfo] | None = None
69
+ self._private_infos: ta.MutableMapping[Private, privates_.PrivateInfo] | None = None
64
70
 
65
71
  ##
66
72
 
67
- def _get_private_info(self, p: Private) -> 'private_.PrivateInfo':
73
+ def _get_private_info(self, p: Private) -> 'privates_.PrivateInfo':
68
74
  if (pis := self._private_infos) is None:
69
75
  self._private_infos = pis = col.IdentityKeyDict()
70
76
  try:
71
77
  return pis[p]
72
78
  except KeyError:
73
- pis[p] = ec = private_.PrivateInfo(self, p)
79
+ pis[p] = ec = privates_.PrivateInfo(self, p)
74
80
  return ec
75
81
 
76
82
  ##
@@ -103,6 +109,9 @@ class ElementCollection(lang.Final):
103
109
  elif isinstance(e, (SetBinding, MapBinding)):
104
110
  add(e.multi_key, e)
105
111
 
112
+ elif isinstance(e, ProvisionListenerBinding):
113
+ add(None, e)
114
+
106
115
  elif isinstance(e, Overrides):
107
116
  ovr = self._build_raw_element_multimap(e.ovr)
108
117
  src = self._build_raw_element_multimap(e.src)
@@ -128,6 +137,24 @@ class ElementCollection(lang.Final):
128
137
 
129
138
  ##
130
139
 
140
+ def _get_single_binding(self, k: Key, bs: ta.Sequence[Binding]) -> Binding:
141
+ if not bs:
142
+ raise UnboundKeyError(k)
143
+
144
+ elif len(bs) > 1:
145
+ d: dict = {}
146
+ for b in bs:
147
+ d.setdefault(b, []).append(b)
148
+ if len(d) > 1:
149
+ raise ConflictingKeyError(k)
150
+ l = check.single(d.values())
151
+ b = copy.copy(l[0])
152
+ set_origins(b, Origins(tuple(o for c in l for o in c.origins)))
153
+ return b
154
+
155
+ else:
156
+ return check.isinstance(check.single(bs), Binding)
157
+
131
158
  def _build_binding_impl_map(self, em: ta.Mapping[Key | None, ta.Sequence[Element]]) -> dict[Key, BindingImpl]:
132
159
  pm: dict[Key, BindingImpl] = {}
133
160
  for k, es in em.items():
@@ -138,14 +165,12 @@ class ElementCollection(lang.Final):
138
165
 
139
166
  es_by_ty.pop(Eager, None)
140
167
  es_by_ty.pop(Expose, None)
168
+ es_by_ty.pop(ProvisionListenerBinding, None)
141
169
 
142
170
  if (bs := es_by_ty.pop(Binding, None)):
143
- if len(bs) > 1:
144
- raise DuplicateKeyError(k)
171
+ b = self._get_single_binding(k, bs) # type: ignore
145
172
 
146
- b: Binding = check.isinstance(check.single(bs), Binding)
147
173
  p: ProviderImpl
148
-
149
174
  if isinstance(b.provider, (SetProvider, MapProvider)):
150
175
  p = make_multi_provider_impl(b.provider, es_by_ty)
151
176
 
@@ -12,6 +12,8 @@ TODO:
12
12
  - multiple live request scopes on single injector - use private injectors?
13
13
  """
14
14
  import contextlib
15
+ import functools
16
+ import itertools
15
17
  import typing as ta
16
18
  import weakref
17
19
 
@@ -24,6 +26,8 @@ from ..injector import Injector
24
26
  from ..inspect import KwargsTarget
25
27
  from ..keys import Key
26
28
  from ..keys import as_key
29
+ from ..listeners import ProvisionListener
30
+ from ..listeners import ProvisionListenerBinding
27
31
  from ..scopes import ScopeBinding
28
32
  from ..scopes import Singleton
29
33
  from ..scopes import Thread
@@ -55,6 +59,13 @@ class InjectorImpl(Injector, lang.Final):
55
59
 
56
60
  self._bim = ec.binding_impl_map()
57
61
  self._ekbs = ec.eager_keys_by_scope()
62
+ self._pls: tuple[ProvisionListener, ...] = tuple(
63
+ b.listener
64
+ for b in itertools.chain(
65
+ ec.elements_of_type(ProvisionListenerBinding),
66
+ (p._pls if p is not None else ()), # noqa
67
+ )
68
+ )
58
69
 
59
70
  self._cs: weakref.WeakSet[InjectorImpl] | None = None
60
71
  self._root: InjectorImpl = p._root if p is not None else self # noqa
@@ -70,6 +81,7 @@ class InjectorImpl(Injector, lang.Final):
70
81
  }
71
82
 
72
83
  self._instantiate_eagers(Unscoped())
84
+ self._instantiate_eagers(Singleton())
73
85
 
74
86
  _root: 'InjectorImpl'
75
87
 
@@ -109,7 +121,7 @@ class InjectorImpl(Injector, lang.Final):
109
121
  check.not_in(key, self._provisions)
110
122
  self._provisions[key] = v
111
123
 
112
- def __enter__(self: ta.Self) -> ta.Self:
124
+ def __enter__(self) -> ta.Self:
113
125
  return self
114
126
 
115
127
  def __exit__(self, *exc) -> None:
@@ -142,7 +154,12 @@ class InjectorImpl(Injector, lang.Final):
142
154
  bi = self._bim.get(key)
143
155
  if bi is not None:
144
156
  sc = self._scopes[bi.scope]
145
- v = sc.provide(bi, self)
157
+
158
+ fn = lambda: sc.provide(bi, self) # noqa
159
+ for pl in self._pls:
160
+ fn = functools.partial(pl, self, key, bi.binding, fn)
161
+ v = fn()
162
+
146
163
  cr.handle_provision(key, v)
147
164
  return lang.just(v)
148
165