omlish 0.0.0.dev6__py3-none-any.whl → 0.0.0.dev7__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 (106) hide show
  1. omlish/__about__.py +109 -5
  2. omlish/__init__.py +0 -8
  3. omlish/asyncs/__init__.py +0 -9
  4. omlish/asyncs/anyio.py +40 -0
  5. omlish/bootstrap.py +737 -0
  6. omlish/check.py +1 -1
  7. omlish/collections/__init__.py +4 -0
  8. omlish/collections/exceptions.py +2 -0
  9. omlish/collections/utils.py +38 -9
  10. omlish/configs/strings.py +2 -0
  11. omlish/dataclasses/__init__.py +7 -0
  12. omlish/dataclasses/impl/descriptors.py +95 -0
  13. omlish/dataclasses/impl/reflect.py +1 -1
  14. omlish/dataclasses/utils.py +23 -0
  15. omlish/{lang/datetimes.py → datetimes.py} +8 -4
  16. omlish/diag/procfs.py +1 -1
  17. omlish/diag/threads.py +131 -48
  18. omlish/docker.py +16 -1
  19. omlish/fnpairs.py +0 -4
  20. omlish/{serde → formats}/dotenv.py +3 -0
  21. omlish/{serde → formats}/yaml.py +2 -2
  22. omlish/graphs/trees.py +1 -1
  23. omlish/http/consts.py +6 -0
  24. omlish/http/sessions.py +2 -2
  25. omlish/inject/__init__.py +4 -0
  26. omlish/inject/binder.py +3 -3
  27. omlish/inject/elements.py +1 -1
  28. omlish/inject/impl/injector.py +57 -27
  29. omlish/inject/impl/origins.py +2 -0
  30. omlish/inject/origins.py +3 -0
  31. omlish/inject/utils.py +18 -0
  32. omlish/iterators.py +69 -2
  33. omlish/lang/__init__.py +16 -7
  34. omlish/lang/classes/restrict.py +10 -0
  35. omlish/lang/contextmanagers.py +1 -1
  36. omlish/lang/descriptors.py +3 -3
  37. omlish/lang/imports.py +67 -0
  38. omlish/lang/iterables.py +40 -0
  39. omlish/lang/maybes.py +3 -0
  40. omlish/lang/objects.py +38 -0
  41. omlish/lang/strings.py +25 -0
  42. omlish/lang/sys.py +9 -0
  43. omlish/lang/typing.py +37 -0
  44. omlish/lite/__init__.py +1 -0
  45. omlish/lite/cached.py +18 -0
  46. omlish/lite/check.py +29 -0
  47. omlish/lite/contextmanagers.py +18 -0
  48. omlish/lite/json.py +30 -0
  49. omlish/lite/logs.py +52 -0
  50. omlish/lite/marshal.py +316 -0
  51. omlish/lite/reflect.py +49 -0
  52. omlish/lite/runtime.py +18 -0
  53. omlish/lite/secrets.py +19 -0
  54. omlish/lite/strings.py +25 -0
  55. omlish/lite/subprocesses.py +112 -0
  56. omlish/logs/configs.py +15 -2
  57. omlish/logs/formatters.py +7 -2
  58. omlish/marshal/__init__.py +28 -0
  59. omlish/marshal/any.py +5 -5
  60. omlish/marshal/base.py +27 -11
  61. omlish/marshal/base64.py +24 -9
  62. omlish/marshal/dataclasses.py +34 -28
  63. omlish/marshal/datetimes.py +74 -18
  64. omlish/marshal/enums.py +14 -8
  65. omlish/marshal/exceptions.py +11 -1
  66. omlish/marshal/factories.py +59 -74
  67. omlish/marshal/forbidden.py +35 -0
  68. omlish/marshal/global_.py +11 -4
  69. omlish/marshal/iterables.py +21 -24
  70. omlish/marshal/mappings.py +23 -26
  71. omlish/marshal/numbers.py +51 -0
  72. omlish/marshal/optionals.py +11 -12
  73. omlish/marshal/polymorphism.py +86 -21
  74. omlish/marshal/primitives.py +4 -5
  75. omlish/marshal/standard.py +13 -8
  76. omlish/marshal/uuids.py +4 -5
  77. omlish/matchfns.py +218 -0
  78. omlish/os.py +64 -0
  79. omlish/reflect/__init__.py +39 -0
  80. omlish/reflect/isinstance.py +38 -0
  81. omlish/reflect/ops.py +84 -0
  82. omlish/reflect/subst.py +110 -0
  83. omlish/reflect/types.py +275 -0
  84. omlish/secrets/__init__.py +18 -2
  85. omlish/secrets/crypto.py +132 -0
  86. omlish/secrets/marshal.py +36 -7
  87. omlish/secrets/openssl.py +207 -0
  88. omlish/secrets/secrets.py +260 -8
  89. omlish/secrets/subprocesses.py +42 -0
  90. omlish/sql/dbs.py +6 -5
  91. omlish/sql/exprs.py +12 -0
  92. omlish/sql/secrets.py +10 -0
  93. omlish/term.py +1 -1
  94. omlish/testing/pytest/plugins/switches.py +54 -19
  95. omlish/text/glyphsplit.py +5 -0
  96. omlish-0.0.0.dev7.dist-info/METADATA +50 -0
  97. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev7.dist-info}/RECORD +104 -76
  98. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev7.dist-info}/WHEEL +1 -1
  99. omlish/reflect.py +0 -470
  100. omlish-0.0.0.dev6.dist-info/METADATA +0 -34
  101. /omlish/{asyncs/futures.py → concurrent.py} +0 -0
  102. /omlish/{serde → formats}/__init__.py +0 -0
  103. /omlish/{serde → formats}/json.py +0 -0
  104. /omlish/{serde → formats}/props.py +0 -0
  105. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev7.dist-info}/LICENSE +0 -0
  106. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev7.dist-info}/top_level.txt +0 -0
omlish/inject/elements.py CHANGED
@@ -8,7 +8,7 @@ from .impl.origins import HasOriginsImpl
8
8
 
9
9
 
10
10
  class Element(HasOriginsImpl, lang.Abstract, lang.PackageSealed):
11
- pass
11
+ """Note: inheritors must be dataclasses."""
12
12
 
13
13
 
14
14
  class ElementGenerator(lang.Abstract, lang.PackageSealed):
@@ -1,5 +1,7 @@
1
1
  """
2
2
  TODO:
3
+ - ** can currently bind in a child/private scope shadowing an external parent binding **
4
+ - better source tracking
3
5
  - cache/export ElementCollections lol
4
6
  - scope bindings, auto in root
5
7
  - injector-internal / blacklisted bindings (Injector itself, default scopes) without rebuilding ElementCollection
@@ -14,6 +16,7 @@ TODO:
14
16
  import contextlib
15
17
  import functools
16
18
  import itertools
19
+ import logging
17
20
  import typing as ta
18
21
  import weakref
19
22
 
@@ -39,6 +42,9 @@ from .scopes import ScopeImpl
39
42
  from .scopes import make_scope_impl
40
43
 
41
44
 
45
+ log = logging.getLogger(__name__)
46
+
47
+
42
48
  DEFAULT_SCOPES: list[Scope] = [
43
49
  Unscoped(),
44
50
  Singleton(),
@@ -103,10 +109,11 @@ class InjectorImpl(Injector, lang.Final):
103
109
  def __init__(self, injector: 'InjectorImpl') -> None:
104
110
  super().__init__()
105
111
  self._injector = injector
106
- self._provisions: dict[Key, ta.Any] = {}
112
+ self._provisions: dict[Key, lang.Maybe] = {}
107
113
  self._seen_keys: set[Key] = set()
114
+ self._source_stack: list[ta.Any] = []
108
115
 
109
- def handle_key(self, key: Key) -> lang.Maybe:
116
+ def handle_key(self, key: Key) -> lang.Maybe[lang.Maybe]:
110
117
  try:
111
118
  return lang.just(self._provisions[key])
112
119
  except KeyError:
@@ -116,10 +123,21 @@ class InjectorImpl(Injector, lang.Final):
116
123
  self._seen_keys.add(key)
117
124
  return lang.empty()
118
125
 
119
- def handle_provision(self, key: Key, v: ta.Any) -> None:
126
+ def handle_provision(self, key: Key, mv: lang.Maybe) -> lang.Maybe:
120
127
  check.in_(key, self._seen_keys)
121
128
  check.not_in(key, self._provisions)
122
- self._provisions[key] = v
129
+ self._provisions[key] = mv
130
+ return mv
131
+
132
+ @contextlib.contextmanager
133
+ def push_source(self, source: ta.Any) -> ta.Iterator[None]:
134
+ self._source_stack.append(source)
135
+ try:
136
+ yield
137
+ finally:
138
+ nsource = self._source_stack.pop()
139
+ if source is not nsource:
140
+ raise Exception(f'Stack error: {source=} is not {nsource=}')
123
141
 
124
142
  def __enter__(self) -> ta.Self:
125
143
  return self
@@ -140,38 +158,50 @@ class InjectorImpl(Injector, lang.Final):
140
158
  finally:
141
159
  self.__cur_req = None
142
160
 
143
- def try_provide(self, key: ta.Any) -> lang.Maybe[ta.Any]:
161
+ def _try_provide(self, key: ta.Any, *, source: ta.Any = None) -> lang.Maybe[ta.Any]:
144
162
  key = as_key(key)
145
163
 
164
+ cr: InjectorImpl._Request
146
165
  with self._current_request() as cr:
147
- ic = self._internal_consts.get(key)
148
- if ic is not None:
149
- return lang.just(ic)
166
+ with cr.push_source(source):
167
+ if (rv := cr.handle_key(key)).present:
168
+ return rv.must()
150
169
 
151
- if (rv := cr.handle_key(key)).present:
152
- return rv
170
+ ic = self._internal_consts.get(key)
171
+ if ic is not None:
172
+ return cr.handle_provision(key, lang.just(ic))
153
173
 
154
- bi = self._bim.get(key)
155
- if bi is not None:
156
- sc = self._scopes[bi.scope]
174
+ bi = self._bim.get(key)
175
+ if bi is not None:
176
+ sc = self._scopes[bi.scope]
157
177
 
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()
178
+ fn = lambda: sc.provide(bi, self) # noqa
179
+ for pl in self._pls:
180
+ fn = functools.partial(pl, self, key, bi.binding, fn)
181
+ v = fn()
162
182
 
163
- cr.handle_provision(key, v)
164
- return lang.just(v)
183
+ return cr.handle_provision(key, lang.just(v))
165
184
 
166
- if self._p is not None:
167
- pv = self._p.try_provide(key)
168
- if pv is not None:
169
- return pv
185
+ if self._p is not None:
186
+ pv = self._p._try_provide(key, source=source) # noqa
187
+ if pv.present:
188
+ return cr.handle_provision(key, pv)
170
189
 
171
- return lang.empty()
190
+ return cr.handle_provision(key, lang.empty())
191
+
192
+ def _provide(self, key: ta.Any, *, source: ta.Any = None) -> ta.Any:
193
+ v = self._try_provide(key, source=source)
194
+ if v.present:
195
+ return v.must()
196
+ raise UnboundKeyError(key)
197
+
198
+ #
199
+
200
+ def try_provide(self, key: ta.Any) -> lang.Maybe[ta.Any]:
201
+ return self.try_provide(key)
172
202
 
173
203
  def provide(self, key: ta.Any) -> ta.Any:
174
- v = self.try_provide(key)
204
+ v = self._try_provide(key)
175
205
  if v.present:
176
206
  return v.must()
177
207
  raise UnboundKeyError(key)
@@ -180,11 +210,11 @@ class InjectorImpl(Injector, lang.Final):
180
210
  ret: dict[str, ta.Any] = {}
181
211
  for kw in kt.kwargs:
182
212
  if kw.has_default:
183
- if not (mv := self.try_provide(kw.key)).present:
213
+ if not (mv := self._try_provide(kw.key, source=kt)).present:
184
214
  continue
185
215
  v = mv.must()
186
216
  else:
187
- v = self.provide(kw.key)
217
+ v = self._provide(kw.key, source=kt)
188
218
  ret[kw.name] = v
189
219
  return ret
190
220
 
@@ -64,6 +64,8 @@ def set_origins(obj: HasOriginsT, origins: Origins) -> HasOriginsT:
64
64
 
65
65
 
66
66
  class HasOriginsImpl(HasOrigins):
67
+ """Note: inheritors must be dataclasses."""
68
+
67
69
  @property
68
70
  def origins(self) -> Origins:
69
71
  return self.__dict__[ORIGINS_ATTR]
omlish/inject/origins.py CHANGED
@@ -5,6 +5,9 @@ from .. import dataclasses as dc
5
5
  from .. import lang
6
6
 
7
7
 
8
+ T = ta.TypeVar('T')
9
+
10
+
8
11
  @dc.dataclass(frozen=True)
9
12
  @dc.extra_params(cache_hash=True)
10
13
  class Origin:
omlish/inject/utils.py ADDED
@@ -0,0 +1,18 @@
1
+ import typing as ta
2
+
3
+ from .. import dataclasses as dc
4
+ from .. import lang
5
+ from .impl.origins import HasOriginsImpl
6
+
7
+
8
+ T = ta.TypeVar('T')
9
+
10
+
11
+ @dc.dataclass(frozen=True, eq=False)
12
+ class ConstFn(HasOriginsImpl, lang.Final, ta.Generic[T]):
13
+ """An origin tracking provider function for a constant value. Equivalent to `lambda: v` but transparent."""
14
+
15
+ v: T
16
+
17
+ def __call__(self) -> T:
18
+ return self.v
omlish/iterators.py CHANGED
@@ -1,9 +1,13 @@
1
1
  import collections
2
+ import dataclasses as dc
2
3
  import functools
3
4
  import heapq
4
5
  import itertools
5
6
  import typing as ta
6
7
 
8
+ # from . import check
9
+ from . import lang
10
+
7
11
 
8
12
  T = ta.TypeVar('T')
9
13
  U = ta.TypeVar('U')
@@ -13,10 +17,10 @@ _MISSING = object()
13
17
 
14
18
  class PeekIterator(ta.Iterator[T]):
15
19
 
16
- def __init__(self, it: ta.Iterator[T]) -> None:
20
+ def __init__(self, it: ta.Iterable[T]) -> None:
17
21
  super().__init__()
18
22
 
19
- self._it = it
23
+ self._it = iter(it)
20
24
  self._pos = -1
21
25
  self._next_item: ta.Any = _MISSING
22
26
 
@@ -231,3 +235,66 @@ def sliding_window(it: ta.Iterable[T], n: int) -> ta.Iterator[tuple[T, ...]]:
231
235
  for x in iterator:
232
236
  window.append(x)
233
237
  yield tuple(window)
238
+
239
+
240
+ ##
241
+
242
+
243
+ @dc.dataclass()
244
+ class UniqueStats:
245
+ key: ta.Any
246
+ num_seen: int
247
+ first_idx: int
248
+ last_idx: int
249
+
250
+
251
+ @dc.dataclass(frozen=True)
252
+ class UniqueItem(ta.Generic[T]):
253
+ idx: int
254
+ item: T
255
+ stats: UniqueStats
256
+ out: lang.Maybe[T]
257
+
258
+
259
+ class UniqueIterator(ta.Iterator[UniqueItem[T]]):
260
+ def __init__(
261
+ self,
262
+ it: ta.Iterable[T],
263
+ keyer: ta.Callable[[T], ta.Any] = lang.identity,
264
+ ) -> None:
265
+ super().__init__()
266
+ self._it = enumerate(it)
267
+ self._keyer = keyer
268
+
269
+ self.stats: dict[ta.Any, UniqueStats] = {}
270
+
271
+ def __next__(self) -> UniqueItem[T]:
272
+ idx, item = next(self._it)
273
+ key = self._keyer(item)
274
+
275
+ try:
276
+ stats = self.stats[key]
277
+
278
+ except KeyError:
279
+ stats = self.stats[key] = UniqueStats(
280
+ key,
281
+ num_seen=1,
282
+ first_idx=idx,
283
+ last_idx=idx,
284
+ )
285
+ return UniqueItem(
286
+ idx,
287
+ item,
288
+ stats,
289
+ lang.just(item),
290
+ )
291
+
292
+ else:
293
+ stats.num_seen += 1
294
+ stats.last_idx = idx
295
+ return UniqueItem(
296
+ idx,
297
+ item,
298
+ stats,
299
+ lang.empty(),
300
+ )
omlish/lang/__init__.py CHANGED
@@ -68,13 +68,6 @@ from .contextmanagers import ( # noqa
68
68
  maybe_managing,
69
69
  )
70
70
 
71
- from .datetimes import ( # noqa
72
- months_ago,
73
- parse_date,
74
- parse_timedelta,
75
- to_seconds,
76
- )
77
-
78
71
  from .descriptors import ( # noqa
79
72
  AccessForbiddenError,
80
73
  access_forbidden,
@@ -121,6 +114,7 @@ from .imports import ( # noqa
121
114
  import_module_attr,
122
115
  lazy_import,
123
116
  proxy_import,
117
+ resolve_import_name,
124
118
  try_import,
125
119
  yield_import_all,
126
120
  yield_importable,
@@ -128,8 +122,11 @@ from .imports import ( # noqa
128
122
 
129
123
  from .iterables import ( # noqa
130
124
  BUILTIN_SCALAR_ITERABLE_TYPES,
125
+ Generator,
131
126
  asrange,
132
127
  exhaust,
128
+ flatmap,
129
+ flatten,
133
130
  ilen,
134
131
  itergen,
135
132
  peek,
@@ -149,12 +146,18 @@ from .objects import ( # noqa
149
146
  SimpleProxy,
150
147
  arg_repr,
151
148
  attr_repr,
149
+ can_weakref,
150
+ deep_subclasses,
152
151
  new_type,
153
152
  opt_repr,
154
153
  super_meta,
155
154
  )
156
155
 
157
156
  from .strings import ( # noqa
157
+ BOOL_FALSE_STRINGS,
158
+ BOOL_STRINGS,
159
+ BOOL_TRUE_STRINGS,
160
+ STRING_BOOL_VALUES,
158
161
  camel_case,
159
162
  indent_lines,
160
163
  is_dunder,
@@ -167,6 +170,8 @@ from .strings import ( # noqa
167
170
  )
168
171
 
169
172
  from .sys import ( # noqa
173
+ REQUIRED_PYTHON_VERSION,
174
+ check_runtime_version,
170
175
  is_gil_enabled,
171
176
  )
172
177
 
@@ -180,6 +185,10 @@ from .timeouts import ( # noqa
180
185
 
181
186
  from .typing import ( # noqa
182
187
  BytesLike,
188
+ Func0,
189
+ Func1,
190
+ Func2,
191
+ Func3,
183
192
  protocol_check,
184
193
  typed_lambda,
185
194
  typed_partial,
@@ -97,9 +97,19 @@ class NotInstantiable(Abstract):
97
97
  class NotPicklable:
98
98
  __slots__ = ()
99
99
 
100
+ @ta.final
101
+ def __reduce__(self) -> ta.NoReturn:
102
+ raise TypeError
103
+
104
+ @ta.final
105
+ def __reduce_ex__(self, protocol) -> ta.NoReturn:
106
+ raise TypeError
107
+
108
+ @ta.final
100
109
  def __getstate__(self) -> ta.NoReturn:
101
110
  raise TypeError
102
111
 
112
+ @ta.final
103
113
  def __setstate__(self, state) -> ta.NoReturn:
104
114
  raise TypeError
105
115
 
@@ -307,7 +307,7 @@ Lockable = ta.Callable[[], ta.ContextManager]
307
307
  DefaultLockable = bool | Lockable | ta.ContextManager | None
308
308
 
309
309
 
310
- def default_lock(value: DefaultLockable, default: DefaultLockable) -> Lockable:
310
+ def default_lock(value: DefaultLockable, default: DefaultLockable = None) -> Lockable:
311
311
  if value is None:
312
312
  value = default
313
313
 
@@ -114,7 +114,7 @@ class _decorator_descriptor: # noqa
114
114
  self._wrapper, self._fn = wrapper, fn
115
115
  update_wrapper_except_dict(self, fn)
116
116
 
117
- def __get__(self, instance, owner):
117
+ def __get__(self, instance, owner=None):
118
118
  return functools.update_wrapper(functools.partial(self._wrapper, fn := self._fn.__get__(instance, owner)), fn) # noqa
119
119
 
120
120
  else:
@@ -123,7 +123,7 @@ class _decorator_descriptor: # noqa
123
123
  self._md = _has_method_descriptor(fn)
124
124
  update_wrapper_except_dict(self, fn)
125
125
 
126
- def __get__(self, instance, owner):
126
+ def __get__(self, instance, owner=None):
127
127
  fn = self._fn.__get__(instance, owner)
128
128
  if self._md or instance is not None:
129
129
  @functools.wraps(fn)
@@ -218,7 +218,7 @@ class _ClassOnly:
218
218
  def _mth(self, x):
219
219
  self.__mth = x
220
220
 
221
- def __get__(self, instance, owner):
221
+ def __get__(self, instance, owner=None):
222
222
  if instance is not None:
223
223
  raise TypeError(f'method must not be used on instance: {self._mth}')
224
224
  return self._mth[0].__get__(instance, owner)
omlish/lang/imports.py CHANGED
@@ -155,3 +155,70 @@ def try_import(spec: str) -> types.ModuleType | None:
155
155
  return __import__(s, globals(), level=l)
156
156
  except ImportError:
157
157
  return None
158
+
159
+
160
+ ##
161
+
162
+
163
+ def resolve_import_name(name: str, package: str | None = None) -> str:
164
+ level = 0
165
+
166
+ if name.startswith('.'):
167
+ if not package:
168
+ raise TypeError("the 'package' argument is required to perform a relative import for {name!r}")
169
+ for character in name:
170
+ if character != '.':
171
+ break
172
+ level += 1
173
+
174
+ name = name[level:]
175
+
176
+ if not isinstance(name, str):
177
+ raise TypeError(f'module name must be str, not {type(name)}')
178
+ if level < 0:
179
+ raise ValueError('level must be >= 0')
180
+ if level > 0:
181
+ if not isinstance(package, str):
182
+ raise TypeError('__package__ not set to a string')
183
+ elif not package:
184
+ raise ImportError('attempted relative import with no known parent package')
185
+ if not name and level == 0:
186
+ raise ValueError('Empty module name')
187
+
188
+ if level > 0:
189
+ bits = package.rsplit('.', level - 1) # type: ignore
190
+ if len(bits) < level:
191
+ raise ImportError('attempted relative import beyond top-level package')
192
+ base = bits[0]
193
+ name = f'{base}.{name}' if name else base
194
+
195
+ return name
196
+
197
+
198
+ ##
199
+
200
+
201
+ _REGISTERED_CONDITIONAL_IMPORTS: dict[str, list[str] | None] = {}
202
+
203
+
204
+ def _register_conditional_import(when: str, then: str, package: str | None = None) -> None:
205
+ wn = resolve_import_name(when, package)
206
+ tn = resolve_import_name(then, package)
207
+ if tn in sys.modules:
208
+ return
209
+ if wn in sys.modules:
210
+ __import__(tn)
211
+ else:
212
+ tns = _REGISTERED_CONDITIONAL_IMPORTS.setdefault(wn, [])
213
+ if tns is None:
214
+ raise Exception(f'Conditional import trigger already cleared: {wn=} {tn=}')
215
+ tns.append(tn)
216
+
217
+
218
+ def _trigger_conditional_imports(package: str) -> None:
219
+ tns = _REGISTERED_CONDITIONAL_IMPORTS.get(package, [])
220
+ if tns is None:
221
+ raise Exception(f'Conditional import trigger already cleared: {package=}')
222
+ _REGISTERED_CONDITIONAL_IMPORTS[package] = None
223
+ for tn in tns:
224
+ __import__(tn)
omlish/lang/iterables.py CHANGED
@@ -4,6 +4,8 @@ import typing as ta
4
4
 
5
5
 
6
6
  T = ta.TypeVar('T')
7
+ S = ta.TypeVar('S')
8
+ R = ta.TypeVar('R')
7
9
 
8
10
 
9
11
  BUILTIN_SCALAR_ITERABLE_TYPES: tuple[type, ...] = (
@@ -71,3 +73,41 @@ class itergen(ta.Generic[T]): # noqa
71
73
 
72
74
  def renumerate(it: ta.Iterable[T]) -> ta.Iterable[tuple[T, int]]:
73
75
  return ((e, i) for i, e in enumerate(it))
76
+
77
+
78
+ flatten = itertools.chain.from_iterable
79
+
80
+
81
+ def flatmap(fn: ta.Callable[[T], ta.Iterable[R]], it: ta.Iterable[T]) -> ta.Iterable[R]:
82
+ return flatten(map(fn, it))
83
+
84
+
85
+ class Generator(ta.Generator[T, S, R]):
86
+ def __init__(self, gen: ta.Generator[T, S, R]) -> None:
87
+ super().__init__()
88
+ self.gen = gen
89
+
90
+ value: R
91
+
92
+ def __iter__(self):
93
+ return self
94
+
95
+ def __next__(self):
96
+ return self.send(None)
97
+
98
+ def send(self, v):
99
+ try:
100
+ return self.gen.send(v)
101
+ except StopIteration as e:
102
+ self.value = e.value
103
+ raise
104
+
105
+ def throw(self, *args):
106
+ try:
107
+ return self.gen.throw(*args)
108
+ except StopIteration as e:
109
+ self.value = e.value
110
+ raise
111
+
112
+ def close(self):
113
+ self.gen.close()
omlish/lang/maybes.py CHANGED
@@ -65,6 +65,9 @@ class Maybe(abc.ABC, ta.Generic[T]):
65
65
  class _Maybe(Maybe[T], tuple):
66
66
  __slots__ = ()
67
67
 
68
+ def __init_subclass__(cls, **kwargs):
69
+ raise TypeError
70
+
68
71
  @property
69
72
  def present(self) -> bool:
70
73
  return bool(self)
omlish/lang/objects.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import types
2
2
  import typing as ta
3
+ import weakref
3
4
 
4
5
 
5
6
  T = ta.TypeVar('T')
@@ -28,6 +29,28 @@ def opt_repr(obj: ta.Any) -> str | None:
28
29
  ##
29
30
 
30
31
 
32
+ _CAN_WEAKREF_TYPE_MAP: ta.MutableMapping[type, bool] = weakref.WeakKeyDictionary()
33
+
34
+
35
+ def can_weakref(obj: ta.Any) -> bool:
36
+ _type = type(obj)
37
+ try:
38
+ return _CAN_WEAKREF_TYPE_MAP[_type]
39
+ except KeyError:
40
+ pass
41
+ try:
42
+ weakref.ref(obj)
43
+ except TypeError:
44
+ ret = False
45
+ else:
46
+ ret = True
47
+ _CAN_WEAKREF_TYPE_MAP[_type] = ret
48
+ return ret
49
+
50
+
51
+ ##
52
+
53
+
31
54
  def new_type(
32
55
  name: str,
33
56
  bases: ta.Sequence[ta.Any],
@@ -62,6 +85,21 @@ def super_meta(
62
85
  ##
63
86
 
64
87
 
88
+ def deep_subclasses(cls: type) -> ta.Iterator[type]:
89
+ seen = set()
90
+ todo = list(reversed(cls.__subclasses__()))
91
+ while todo:
92
+ cur = todo.pop()
93
+ if cur in seen:
94
+ continue
95
+ seen.add(cur)
96
+ yield cur
97
+ todo.extend(reversed(cur.__subclasses__()))
98
+
99
+
100
+ ##
101
+
102
+
65
103
  class SimpleProxy(ta.Generic[T]):
66
104
 
67
105
  class Descriptor:
omlish/lang/strings.py CHANGED
@@ -126,3 +126,28 @@ def is_ident_cont(c: str) -> bool:
126
126
 
127
127
  def is_ident(name: str) -> bool:
128
128
  return is_ident_start(name[0]) and all(is_ident_cont(c) for c in name[1:])
129
+
130
+
131
+ ##
132
+
133
+
134
+ BOOL_STRINGS: ta.Sequence[tuple[str, str]] = [
135
+ ('n', 'y'),
136
+ ('no', 'yes'),
137
+ ('f', 't'),
138
+ ('false', 'true'),
139
+ ('off', 'on'),
140
+ ('0', '1'),
141
+ ]
142
+
143
+ BOOL_FALSE_STRINGS = frozenset(tup[0] for tup in BOOL_STRINGS)
144
+ BOOL_TRUE_STRINGS = frozenset(tup[1] for tup in BOOL_STRINGS)
145
+
146
+ STRING_BOOL_VALUES: ta.Mapping[str, bool] = {
147
+ k: v
148
+ for ks, v in [
149
+ (BOOL_FALSE_STRINGS, False),
150
+ (BOOL_TRUE_STRINGS, True),
151
+ ]
152
+ for k in ks
153
+ }
omlish/lang/sys.py CHANGED
@@ -1,6 +1,15 @@
1
1
  import sys
2
2
 
3
3
 
4
+ REQUIRED_PYTHON_VERSION = (3, 12)
5
+
6
+
7
+ def check_runtime_version() -> None:
8
+ if sys.version_info < REQUIRED_PYTHON_VERSION:
9
+ raise OSError(
10
+ f'Requires python {REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
11
+
12
+
4
13
  def is_gil_enabled() -> bool:
5
14
  if (fn := getattr(sys, '_is_gil_enabled', None)) is not None:
6
15
  return fn()