omlish 0.0.0.dev484__py3-none-any.whl → 0.0.0.dev506__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 (93) hide show
  1. omlish/CODESTYLE.md +345 -0
  2. omlish/README.md +199 -0
  3. omlish/__about__.py +12 -5
  4. omlish/_check.cc +209 -0
  5. omlish/check.py +11 -0
  6. omlish/dataclasses/__init__.py +4 -0
  7. omlish/dataclasses/impl/concerns/frozen.py +4 -1
  8. omlish/dataclasses/impl/generation/plans.py +2 -17
  9. omlish/dataclasses/impl/generation/processor.py +2 -2
  10. omlish/dataclasses/impl/processing/driving.py +13 -1
  11. omlish/dataclasses/tools/replace.py +27 -0
  12. omlish/diag/_pycharm/runhack.py +1 -1
  13. omlish/dispatch/functions.py +1 -1
  14. omlish/formats/json/stream/lexing.py +13 -5
  15. omlish/formats/json/stream/parsing.py +1 -1
  16. omlish/inject/README.md +430 -0
  17. omlish/inject/__init__.py +20 -11
  18. omlish/inject/_dataclasses.py +1545 -1383
  19. omlish/inject/binder.py +7 -4
  20. omlish/inject/eagers.py +2 -4
  21. omlish/inject/elements.py +4 -0
  22. omlish/inject/helpers/late.py +76 -0
  23. omlish/inject/{managed.py → helpers/managed.py} +37 -34
  24. omlish/inject/impl/elements.py +7 -4
  25. omlish/inject/impl/injector.py +14 -26
  26. omlish/inject/impl/inspect.py +0 -8
  27. omlish/inject/impl/origins.py +1 -0
  28. omlish/inject/impl/privates.py +2 -6
  29. omlish/inject/impl/providers.py +0 -4
  30. omlish/inject/impl/scopes.py +14 -18
  31. omlish/inject/inspect.py +10 -1
  32. omlish/inject/multis.py +0 -3
  33. omlish/inject/scopes.py +7 -5
  34. omlish/io/buffers.py +35 -8
  35. omlish/lang/__init__.py +10 -0
  36. omlish/lang/classes/simple.py +2 -1
  37. omlish/lang/iterables.py +6 -0
  38. omlish/lang/objects.py +13 -0
  39. omlish/lang/outcomes.py +1 -1
  40. omlish/lang/recursion.py +1 -1
  41. omlish/lang/sequences.py +33 -0
  42. omlish/lifecycles/README.md +30 -0
  43. omlish/lifecycles/__init__.py +87 -13
  44. omlish/lifecycles/_dataclasses.py +1388 -0
  45. omlish/lifecycles/base.py +178 -64
  46. omlish/lifecycles/contextmanagers.py +113 -4
  47. omlish/lifecycles/controller.py +150 -87
  48. omlish/lifecycles/injection.py +143 -0
  49. omlish/lifecycles/listeners.py +56 -0
  50. omlish/lifecycles/managed.py +142 -0
  51. omlish/lifecycles/manager.py +218 -93
  52. omlish/lifecycles/states.py +2 -0
  53. omlish/lifecycles/transitions.py +3 -0
  54. omlish/lifecycles/unwrap.py +57 -0
  55. omlish/lite/maybes.py +7 -0
  56. omlish/lite/typing.py +33 -0
  57. omlish/logs/_amalg.py +1 -1
  58. omlish/logs/all.py +36 -11
  59. omlish/logs/asyncs.py +73 -0
  60. omlish/logs/base.py +101 -12
  61. omlish/logs/bisync.py +99 -0
  62. omlish/logs/contexts.py +4 -1
  63. omlish/logs/lists.py +125 -0
  64. omlish/logs/modules.py +19 -1
  65. omlish/logs/std/loggers.py +6 -1
  66. omlish/logs/std/noisy.py +11 -9
  67. omlish/logs/{standard.py → std/standard.py} +3 -4
  68. omlish/logs/utils.py +16 -1
  69. omlish/marshal/_dataclasses.py +813 -813
  70. omlish/reflect/__init__.py +43 -26
  71. omlish/reflect/ops.py +10 -1
  72. omlish/specs/jmespath/_dataclasses.py +597 -597
  73. omlish/specs/jsonschema/keywords/_dataclasses.py +244 -244
  74. omlish/sql/__init__.py +24 -5
  75. omlish/sql/api/dbapi.py +1 -1
  76. omlish/sql/dbapi/__init__.py +15 -0
  77. omlish/sql/{dbapi.py → dbapi/drivers.py} +2 -2
  78. omlish/sql/queries/__init__.py +3 -0
  79. omlish/testing/pytest/plugins/asyncs/plugin.py +2 -0
  80. omlish/text/docwrap/cli.py +5 -0
  81. omlish/typedvalues/_collection.cc +500 -0
  82. omlish/typedvalues/collection.py +159 -62
  83. omlish/typedvalues/generic.py +5 -4
  84. omlish/typedvalues/values.py +6 -0
  85. {omlish-0.0.0.dev484.dist-info → omlish-0.0.0.dev506.dist-info}/METADATA +14 -9
  86. {omlish-0.0.0.dev484.dist-info → omlish-0.0.0.dev506.dist-info}/RECORD +92 -77
  87. omlish/lifecycles/abstract.py +0 -86
  88. /omlish/inject/{impl → helpers}/proxy.py +0 -0
  89. /omlish/sql/{abc.py → dbapi/abc.py} +0 -0
  90. {omlish-0.0.0.dev484.dist-info → omlish-0.0.0.dev506.dist-info}/WHEEL +0 -0
  91. {omlish-0.0.0.dev484.dist-info → omlish-0.0.0.dev506.dist-info}/entry_points.txt +0 -0
  92. {omlish-0.0.0.dev484.dist-info → omlish-0.0.0.dev506.dist-info}/licenses/LICENSE +0 -0
  93. {omlish-0.0.0.dev484.dist-info → omlish-0.0.0.dev506.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,430 @@
1
+ # omlish.inject
2
+
3
+ A Guice-inspired dependency injection system for Python with first-class async support and a focus on type safety.
4
+
5
+ ## Overview
6
+
7
+ `omlish.inject` provides constructor-based dependency injection with explicit, declarative configuration. Like Guice, it
8
+ emphasizes immutability, composition over inheritance, and pure constructor injection. Unlike Guice, it embraces
9
+ Python's native type system, provides native async/await support, and uses a functional elements-based configuration
10
+ model.
11
+
12
+ The system is built around three core concepts:
13
+ - **Keys** identify dependencies by type and optional tag
14
+ - **Bindings** associate keys with providers
15
+ - **Injectors** resolve and provide instances
16
+
17
+ ## Core API
18
+
19
+ ### Keys
20
+
21
+ A `Key` identifies a dependency by its type and an optional tag for disambiguation:
22
+
23
+ ```python
24
+ from omlish import inject as inj
25
+
26
+ # Simple key by type
27
+ int_key = inj.Key(int)
28
+
29
+ # Tagged key for multiple bindings of the same type
30
+ db_conn_key = inj.Key(DbConnection, tag='primary')
31
+ cache_conn_key = inj.Key(DbConnection, tag='cache')
32
+
33
+ # Convert any type to a key
34
+ key = inj.as_key(MyService)
35
+ ```
36
+
37
+ ### Bindings
38
+
39
+ Bindings connect keys to providers. The `bind()` function provides a concise API for most use cases:
40
+
41
+ ```python
42
+ # Bind a constant
43
+ inj.bind(420)
44
+
45
+ # Bind a type to its constructor
46
+ inj.bind(UserService)
47
+
48
+ # Bind to a factory function
49
+ def make_conn(cfg: Config) -> Connection:
50
+ return create_connection(cfg)
51
+ inj.bind(make_conn)
52
+
53
+ # Bind to another key (linking)
54
+ inj.bind(Service, to_key=ServiceImpl)
55
+
56
+ # Tagged binding
57
+ inj.bind(420, tag='port')
58
+
59
+ # Scoped binding
60
+ inj.bind(Database, singleton=True)
61
+ ```
62
+
63
+ ### Providers
64
+
65
+ Providers define how to construct instances. The system includes several built-in provider types:
66
+
67
+ - `ConstProvider` - returns a constant value
68
+ - `CtorProvider` - calls a constructor
69
+ - `FnProvider` - calls a function
70
+ - `AsyncFnProvider` - calls an async function
71
+ - `LinkProvider` - delegates to another key
72
+
73
+ Providers are typically created implicitly through `bind()`, but can be used directly for advanced cases.
74
+
75
+ ### Injectors
76
+
77
+ Injectors resolve dependencies and provide instances:
78
+
79
+ ```python
80
+ # Create an injector
81
+ injector = inj.create_injector(
82
+ inj.bind(420),
83
+ inj.bind(str, to_fn=inj.KwargsTarget.of(lambda i: f'Port: {i}', i=int)),
84
+ )
85
+
86
+ # Provide instances
87
+ port = injector[int] # 420
88
+ msg = injector.provide(str) # 'Port: 420'
89
+
90
+ # Check if a key is bound
91
+ maybe_val = injector.try_provide(SomeService)
92
+ ```
93
+
94
+ For async code, use `AsyncInjector`:
95
+
96
+ ```python
97
+ async_injector = await inj.create_async_injector(
98
+ inj.bind(async_factory),
99
+ )
100
+
101
+ service = await async_injector[MyService]
102
+ ```
103
+
104
+ ### Elements
105
+
106
+ `Elements` are the building blocks of injector configuration. Most binding functions return `Element` or `Elements`:
107
+
108
+ ```python
109
+ # Combine multiple elements
110
+ config = inj.as_elements(
111
+ inj.bind(DatabaseConfig(host='localhost')),
112
+ inj.bind(Database, singleton=True),
113
+ inj.bind(UserRepository),
114
+ )
115
+
116
+ # Elements can be reused
117
+ injector1 = inj.create_injector(config)
118
+ injector2 = inj.create_injector(config)
119
+
120
+ # Collect elements once for efficiency
121
+ collected = inj.collect_elements(config)
122
+ injector3 = inj.create_injector(collected)
123
+ ```
124
+
125
+ ### Scopes
126
+
127
+ Scopes control instance lifecycle:
128
+
129
+ ```python
130
+ # Singleton scope - one instance per injector
131
+ inj.bind(Database, singleton=True)
132
+ inj.bind(Cache, in_='singleton')
133
+
134
+ # Thread scope - one instance per thread
135
+ inj.bind(RequestContext, in_='thread')
136
+
137
+ # Seeded scope - manually seeded instances
138
+ request_scope = inj.SeededScope('request')
139
+ inj.bind_scope(request_scope)
140
+ inj.bind(UserId, in_=request_scope)
141
+ inj.bind_scope_seed(RequestContext, request_scope)
142
+
143
+ with inj.enter_seeded_scope(injector, request_scope, {
144
+ inj.as_key(RequestContext): ctx,
145
+ }):
146
+ user_id = injector[UserId]
147
+ ```
148
+
149
+ ## Differences from Guice
150
+
151
+ While inspired by Guice, `omlish.inject` differs in several key ways:
152
+
153
+ ### Type System
154
+ - Uses Python's native `typing` module instead of Java generics
155
+ - No annotation-based injection (no `@Inject` decorator)
156
+ - Type annotations on constructors/functions drive automatic injection
157
+
158
+ ### Async Support
159
+ - Native `AsyncInjector` for async dependency graphs
160
+ - `AsyncFnProvider` for async factories
161
+ - Async-aware scopes and lifecycle management
162
+
163
+ ### Configuration Model
164
+ - `Elements`-based functional composition instead of module classes
165
+ - Functions return `Elements` rather than imperative `configure()` methods
166
+ - More flexible composition and reuse
167
+
168
+ ### Dependency Resolution
169
+ - Automatic kwarg inspection via `KwargsTarget`
170
+ - Supports optional dependencies (parameters with defaults)
171
+ - Explicit async/sync separation
172
+
173
+ ### Python Idioms
174
+ - `__getitem__` syntax for common case: `injector[MyService]`
175
+ - Context managers for lifecycle and scoped execution
176
+ - Dataclasses as natural config objects
177
+
178
+ ## Common Idioms
179
+
180
+ ### Hierarchical Package-Level Modules
181
+
182
+ Organize bindings into composable, package-level functions that mirror your application structure:
183
+
184
+ ```python
185
+ # app/services/inject.py
186
+ def bind_services(cfg: ServicesConfig) -> inj.Elements:
187
+ return inj.as_elements(
188
+ inj.bind(UserService, singleton=True),
189
+ inj.bind(AuthService, singleton=True),
190
+ )
191
+
192
+ # app/database/inject.py
193
+ def bind_database(cfg: DatabaseConfig) -> inj.Elements:
194
+ return inj.as_elements(
195
+ inj.bind(cfg),
196
+ inj.bind(Database, singleton=True),
197
+ )
198
+
199
+ # app/inject.py
200
+ def bind_app(cfg: AppConfig) -> inj.Elements:
201
+ return inj.as_elements(
202
+ bind_database(cfg.database),
203
+ bind_services(cfg.services),
204
+ )
205
+ ```
206
+
207
+ This pattern enables:
208
+ - Clear separation of concerns
209
+ - Easy testing of subsystems
210
+ - Configuration composition
211
+ - Lazy imports (using `lang.auto_proxy_import`)
212
+
213
+ ### Multi-Bindings
214
+
215
+ Collect multiple bindings into sets or maps:
216
+
217
+ ```python
218
+ # Set bindings
219
+ inj.set_binder[Plugin]().bind(
220
+ inj.Key(Plugin, tag='auth'),
221
+ inj.Key(Plugin, tag='logging'),
222
+ )
223
+ plugins = injector[ta.AbstractSet[Plugin]]
224
+
225
+ # Map bindings
226
+ inj.map_binder[str, Handler]().bind('GET', GetHandler)
227
+ inj.map_binder[str, Handler]().bind('POST', PostHandler)
228
+ handlers = injector[ta.Mapping[str, Handler]]
229
+
230
+ # Helper for const entries
231
+ inj.bind_set_entry_const(ta.AbstractSet[str], 'value1')
232
+ inj.bind_map_entry_const(ta.Mapping[str, int], 'key', 42)
233
+ ```
234
+
235
+ ### Private Modules
236
+
237
+ Encapsulate internal bindings while exposing only specific keys:
238
+
239
+ ```python
240
+ inj.private(
241
+ inj.bind('jdbc:postgresql://internal-db'),
242
+ inj.bind(DbConnection, singleton=True),
243
+ inj.bind(UserRepository, expose=True),
244
+ )
245
+ ```
246
+
247
+ Only `UserRepository` is visible to the parent injector; the connection string and `DbConnection` remain private.
248
+
249
+ ### Overrides
250
+
251
+ Replace bindings for testing or configuration:
252
+
253
+ ```python
254
+ production = inj.bind(Database, to_ctor=PostgresDatabase)
255
+
256
+ testing = inj.override(
257
+ production,
258
+ inj.bind(Database, to_ctor=InMemoryDatabase),
259
+ )
260
+ ```
261
+
262
+ ### Late Bindings
263
+
264
+ Break circular dependencies by injecting factories:
265
+
266
+ ```python
267
+ class ServiceA:
268
+ def __init__(self, b: 'ServiceB') -> None: ...
269
+
270
+ class ServiceB:
271
+ def __init__(self, c: 'ServiceC') -> None: ...
272
+
273
+ class ServiceC:
274
+ def __init__(self, a: inj.Late[ServiceA]) -> None:
275
+ self.get_a = a # Callable that returns ServiceA
276
+
277
+ inj.create_injector(
278
+ inj.bind(ServiceA, singleton=True),
279
+ inj.bind(ServiceB, singleton=True),
280
+ inj.bind(ServiceC, singleton=True),
281
+ inj.bind_late(ServiceA),
282
+ )
283
+ ```
284
+
285
+ ### Managed Providers
286
+
287
+ Integrate with context managers for lifecycle management:
288
+
289
+ ```python
290
+ # Automatic context manager handling
291
+ with inj.create_managed_injector(
292
+ inj.bind(
293
+ Database,
294
+ singleton=True,
295
+ to_fn=inj.make_managed_provider(Database),
296
+ ),
297
+ ) as injector:
298
+ db = injector[Database]
299
+ # Database.__enter__ called automatically
300
+ # Database.__exit__ called on scope exit
301
+
302
+ # Wrap with custom lifecycle
303
+ inj.bind(
304
+ Resource,
305
+ to_fn=inj.make_managed_provider(
306
+ create_resource,
307
+ contextlib.closing,
308
+ ),
309
+ )
310
+ ```
311
+
312
+ ### KwargsTarget
313
+
314
+ Explicitly control function parameter injection:
315
+
316
+ ```python
317
+ # Manual kwarg specification
318
+ target = inj.KwargsTarget.of(
319
+ my_function,
320
+ db=DatabaseKey,
321
+ cache=(CacheKey, True), # has_default=True
322
+ )
323
+ inj.bind(MyService, to_fn=target)
324
+
325
+ # Decorator syntax
326
+ @inj.target(db=Database, cache=Cache)
327
+ def create_service(db, cache):
328
+ return Service(db, cache)
329
+
330
+ inj.bind(Service, to_fn=create_service)
331
+ ```
332
+
333
+ ### Wrapper Binder Helper
334
+
335
+ Build decorator chains of providers:
336
+
337
+ ```python
338
+ stack = inj.wrapper_binder_helper(Service)
339
+
340
+ # Each layer wraps the previous
341
+ stack.push_bind(to_ctor=LoggingServiceWrapper)
342
+ stack.push_bind(to_ctor=RetryServiceWrapper)
343
+ stack.push_bind(to_ctor=MetricsServiceWrapper)
344
+
345
+ # Final binding gets the fully-wrapped stack
346
+ inj.bind(Service, to_key=stack.top)
347
+ ```
348
+
349
+ ### Items Binder Helper
350
+
351
+ Collect and aggregate items across multiple bindings:
352
+
353
+ ```python
354
+ Plugins = ta.NewType('Plugins', ta.Sequence[Plugin])
355
+ helper = inj.items_binder_helper[Plugin](Plugins)
356
+
357
+ inj.as_elements(
358
+ helper.bind_items_provider(),
359
+ helper.bind_item_consts(plugin1, plugin2),
360
+ helper.bind_item(to_ctor=DynamicPlugin),
361
+ )
362
+
363
+ plugins = injector[Plugins] # All collected plugins
364
+ ```
365
+
366
+ ### Eager Bindings
367
+
368
+ Force instantiation at injector creation:
369
+
370
+ ```python
371
+ # Start background tasks immediately
372
+ inj.bind(BackgroundWorker, singleton=True, eager=True)
373
+
374
+ # Control initialization order with priority
375
+ inj.bind(Database, singleton=True, eager=1)
376
+ inj.bind(Cache, singleton=True, eager=2)
377
+ inj.bind(Service, singleton=True, eager=3)
378
+ ```
379
+
380
+ ### Provision Listeners
381
+
382
+ Intercept instance provision for logging, metrics, or instrumentation:
383
+
384
+ ```python
385
+ async def log_provisions(
386
+ injector: inj.AsyncInjector,
387
+ key: inj.Key,
388
+ binding: inj.Binding | None,
389
+ provide: ta.Callable[[], ta.Awaitable[ta.Any]],
390
+ ) -> ta.Any:
391
+ start = time.time()
392
+ result = await provide()
393
+ duration = time.time() - start
394
+ log.info(f'Provided {key} in {duration:.3f}s')
395
+ return result
396
+
397
+ inj.bind_provision_listener(log_provisions)
398
+ ```
399
+
400
+ ## Error Handling
401
+
402
+ The system provides clear error types for common issues:
403
+
404
+ - `UnboundKeyError` - no binding exists for a requested key
405
+ - `ConflictingKeyError` - multiple conflicting bindings for the same key
406
+ - `CyclicDependencyError` - circular dependency detected
407
+ - `ScopeError` - scope-related errors (not open, already open)
408
+
409
+ ## Advanced Topics
410
+
411
+ ### MaysyncInjector
412
+
413
+ For code that supports both sync and async execution:
414
+
415
+ ```python
416
+ maysync_injector = inj.create_maysync_injector(...)
417
+ # Can be used in both sync and async contexts
418
+ ```
419
+
420
+ ### Custom Scopes
421
+
422
+ Define application-specific scopes by implementing the `Scope` interface and registering with `bind_scope()`.
423
+
424
+ ### Origins
425
+
426
+ Track binding sources for debugging with the `HasOrigins` interface and `Origin` metadata.
427
+
428
+ ### Inspection
429
+
430
+ Use `inj.tag()` and `inj.build_kwargs_target()` for runtime parameter inspection and custom injection logic.
omlish/inject/__init__.py CHANGED
@@ -27,6 +27,25 @@ with _lang.auto_proxy_init(globals()):
27
27
  Id,
28
28
  )
29
29
 
30
+ from .helpers.late import ( # noqa
31
+ Late,
32
+ AsyncLate,
33
+
34
+ bind_late,
35
+ bind_async_late,
36
+ )
37
+
38
+ from .helpers.managed import ( # noqa
39
+ create_async_managed_injector,
40
+ make_async_managed_provider,
41
+
42
+ create_managed_injector,
43
+ make_managed_provider,
44
+
45
+ create_maysync_managed_injector,
46
+ make_maysync_managed_provider,
47
+ )
48
+
30
49
  from .helpers.multis import ( # noqa
31
50
  bind_map_entry_const,
32
51
  bind_set_entry_const,
@@ -85,6 +104,7 @@ with _lang.auto_proxy_init(globals()):
85
104
  KwargsTarget,
86
105
  build_kwargs_target,
87
106
  tag,
107
+ target,
88
108
  )
89
109
 
90
110
  from .keys import ( # noqa
@@ -98,17 +118,6 @@ with _lang.auto_proxy_init(globals()):
98
118
  bind_provision_listener,
99
119
  )
100
120
 
101
- from .managed import ( # noqa
102
- create_async_managed_injector,
103
- make_async_managed_provider,
104
-
105
- create_managed_injector,
106
- make_managed_provider,
107
-
108
- create_maysync_managed_injector,
109
- make_maysync_managed_provider,
110
- )
111
-
112
121
  from .maysync import ( # noqa
113
122
  MaysyncInjector,
114
123
  create_maysync_injector,