omlish 0.0.0.dev493__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 (55) hide show
  1. omlish/CODESTYLE.md +345 -0
  2. omlish/README.md +2 -2
  3. omlish/__about__.py +6 -4
  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/tools/replace.py +27 -0
  9. omlish/dispatch/functions.py +1 -1
  10. omlish/formats/json/stream/lexing.py +13 -5
  11. omlish/formats/json/stream/parsing.py +1 -1
  12. omlish/inject/README.md +430 -0
  13. omlish/inject/__init__.py +1 -0
  14. omlish/inject/_dataclasses.py +64 -64
  15. omlish/inject/eagers.py +0 -4
  16. omlish/inject/elements.py +4 -0
  17. omlish/inject/helpers/late.py +1 -1
  18. omlish/inject/helpers/managed.py +27 -24
  19. omlish/inject/impl/injector.py +7 -22
  20. omlish/inject/impl/inspect.py +0 -8
  21. omlish/inject/impl/origins.py +1 -0
  22. omlish/inject/impl/privates.py +2 -6
  23. omlish/inject/impl/providers.py +0 -4
  24. omlish/inject/impl/scopes.py +14 -18
  25. omlish/inject/inspect.py +9 -0
  26. omlish/inject/multis.py +0 -3
  27. omlish/inject/scopes.py +7 -5
  28. omlish/io/buffers.py +35 -8
  29. omlish/lang/__init__.py +8 -0
  30. omlish/lang/classes/simple.py +2 -1
  31. omlish/lang/iterables.py +6 -0
  32. omlish/lang/objects.py +13 -0
  33. omlish/lang/outcomes.py +1 -1
  34. omlish/lang/recursion.py +1 -1
  35. omlish/lang/sequences.py +33 -0
  36. omlish/lifecycles/_dataclasses.py +18 -18
  37. omlish/lifecycles/injection.py +4 -4
  38. omlish/lite/maybes.py +7 -0
  39. omlish/lite/typing.py +15 -0
  40. omlish/logs/all.py +11 -0
  41. omlish/logs/base.py +3 -3
  42. omlish/logs/bisync.py +99 -0
  43. omlish/marshal/_dataclasses.py +32 -32
  44. omlish/specs/jmespath/_dataclasses.py +38 -38
  45. omlish/specs/jsonschema/keywords/_dataclasses.py +24 -24
  46. omlish/typedvalues/_collection.cc +500 -0
  47. omlish/typedvalues/collection.py +159 -62
  48. omlish/typedvalues/generic.py +5 -4
  49. omlish/typedvalues/values.py +6 -0
  50. {omlish-0.0.0.dev493.dist-info → omlish-0.0.0.dev506.dist-info}/METADATA +9 -7
  51. {omlish-0.0.0.dev493.dist-info → omlish-0.0.0.dev506.dist-info}/RECORD +55 -50
  52. {omlish-0.0.0.dev493.dist-info → omlish-0.0.0.dev506.dist-info}/WHEEL +0 -0
  53. {omlish-0.0.0.dev493.dist-info → omlish-0.0.0.dev506.dist-info}/entry_points.txt +0 -0
  54. {omlish-0.0.0.dev493.dist-info → omlish-0.0.0.dev506.dist-info}/licenses/LICENSE +0 -0
  55. {omlish-0.0.0.dev493.dist-info → omlish-0.0.0.dev506.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  import dataclasses as dc
2
+ import operator
2
3
  import typing as ta
3
4
 
4
5
 
@@ -15,3 +16,29 @@ def deep_replace(o: T, *args: str | ta.Callable[[ta.Any], ta.Mapping[str, ta.Any
15
16
  return dc.replace(o, **args[0](o)) # type: ignore
16
17
  else:
17
18
  return dc.replace(o, **{args[0]: deep_replace(getattr(o, args[0]), *args[1:])}) # type: ignore
19
+
20
+
21
+ ##
22
+
23
+
24
+ def replace_if(
25
+ o: T,
26
+ fn: ta.Callable[[ta.Any, ta.Any], bool] = operator.eq,
27
+ /,
28
+ **kwargs: ta.Any,
29
+ ) -> T:
30
+ dct: dict[str, ta.Any] = {}
31
+ for k, v in kwargs.items():
32
+ if fn(getattr(o, k), v):
33
+ dct[k] = v # noqa
34
+ if not dct:
35
+ return o
36
+ return dc.replace(o, **dct) # type: ignore
37
+
38
+
39
+ def replace_ne(o: T, **kwargs: ta.Any) -> T:
40
+ return replace_if(o, operator.ne, **kwargs)
41
+
42
+
43
+ def replace_is_not(o: T, **kwargs: ta.Any) -> T:
44
+ return replace_if(o, operator.is_not, **kwargs)
@@ -43,7 +43,7 @@ def function(func): # noqa
43
43
  wrapper.dispatch = disp.dispatch # type: ignore
44
44
 
45
45
  else:
46
- from x.c._dispatch import function_wrapper # noqa
46
+ from x.c._dispatch import function_wrapper # type: ignore
47
47
  wrapper = function_wrapper(
48
48
  disp.dispatch,
49
49
  **{k: getattr(func, k) for k in functools.WRAPPER_ASSIGNMENTS if hasattr(func, k)},
@@ -338,7 +338,6 @@ class JsonStreamLexer(GenMachine[str, Token]):
338
338
  self._line = line
339
339
  self._col = col
340
340
 
341
- last = None
342
341
  while True:
343
342
  c: str | None = None
344
343
 
@@ -358,7 +357,7 @@ class JsonStreamLexer(GenMachine[str, Token]):
358
357
  ofs += skip_to - char_in_str_pos
359
358
  if (np := char_in_str.rfind('\n', char_in_str_pos, skip_to)) >= 0:
360
359
  line += char_in_str.count('\n', char_in_str_pos, skip_to)
361
- col = np - char_in_str_pos
360
+ col = skip_to - np - 1
362
361
  else:
363
362
  col += skip_to - char_in_str_pos
364
363
  buf.write(char_in_str[char_in_str_pos:skip_to])
@@ -406,9 +405,18 @@ class JsonStreamLexer(GenMachine[str, Token]):
406
405
  self._raise(f'Unterminated string literal: {buf.getvalue()}')
407
406
 
408
407
  buf.write(c)
409
- if c == q and last != '\\':
410
- break
411
- last = c
408
+ if c == q:
409
+ # Count consecutive backslashes before this quote
410
+ backslash_count = 0
411
+ buf_val = buf.getvalue()
412
+ check_pos = len(buf_val) - 2 # -2 because we just wrote the quote
413
+ while check_pos >= 0 and buf_val[check_pos] == '\\':
414
+ backslash_count += 1
415
+ check_pos -= 1
416
+
417
+ # Quote is escaped only if preceded by odd number of backslashes
418
+ if backslash_count % 2 == 0:
419
+ break
412
420
 
413
421
  restore_state()
414
422
 
@@ -215,7 +215,7 @@ class JsonStreamParser(GenMachine[Token, Event]):
215
215
  except GeneratorExit:
216
216
  raise JsonStreamParseError('Expected object body') from None
217
217
 
218
- if tok.kind == 'STRING' or (self._allow_trailing_commas and tok.kind == 'IDENT'):
218
+ if tok.kind == 'STRING' or (self._allow_extended_idents and tok.kind == 'IDENT'):
219
219
  k = tok.value
220
220
 
221
221
  try:
@@ -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
@@ -104,6 +104,7 @@ with _lang.auto_proxy_init(globals()):
104
104
  KwargsTarget,
105
105
  build_kwargs_target,
106
106
  tag,
107
+ target,
107
108
  )
108
109
 
109
110
  from .keys import ( # noqa