omlish 0.0.0.dev5__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 (163) hide show
  1. omlish/__about__.py +109 -5
  2. omlish/__init__.py +0 -8
  3. omlish/asyncs/__init__.py +9 -9
  4. omlish/asyncs/anyio.py +123 -19
  5. omlish/asyncs/asyncio.py +23 -0
  6. omlish/asyncs/asyncs.py +9 -6
  7. omlish/asyncs/bridge.py +316 -0
  8. omlish/asyncs/trio_asyncio.py +7 -3
  9. omlish/bootstrap.py +737 -0
  10. omlish/check.py +1 -1
  11. omlish/collections/__init__.py +5 -0
  12. omlish/collections/exceptions.py +2 -0
  13. omlish/collections/identity.py +7 -0
  14. omlish/collections/utils.py +38 -9
  15. omlish/configs/strings.py +96 -0
  16. omlish/dataclasses/__init__.py +16 -0
  17. omlish/dataclasses/impl/copy.py +30 -0
  18. omlish/dataclasses/impl/descriptors.py +95 -0
  19. omlish/dataclasses/impl/exceptions.py +6 -0
  20. omlish/dataclasses/impl/fields.py +24 -25
  21. omlish/dataclasses/impl/init.py +4 -2
  22. omlish/dataclasses/impl/main.py +2 -0
  23. omlish/dataclasses/impl/reflect.py +1 -1
  24. omlish/dataclasses/utils.py +67 -0
  25. omlish/{lang/datetimes.py → datetimes.py} +8 -4
  26. omlish/diag/__init__.py +4 -0
  27. omlish/diag/procfs.py +2 -2
  28. omlish/{testing → diag}/pydevd.py +35 -0
  29. omlish/diag/threads.py +131 -48
  30. omlish/dispatch/_dispatch2.py +65 -0
  31. omlish/dispatch/_dispatch3.py +104 -0
  32. omlish/docker.py +16 -1
  33. omlish/fnpairs.py +11 -4
  34. omlish/formats/__init__.py +0 -0
  35. omlish/{configs → formats}/dotenv.py +15 -24
  36. omlish/{json.py → formats/json.py} +2 -1
  37. omlish/formats/yaml.py +223 -0
  38. omlish/graphs/trees.py +1 -1
  39. omlish/http/asgi.py +2 -1
  40. omlish/http/collections.py +15 -0
  41. omlish/http/consts.py +22 -1
  42. omlish/http/sessions.py +10 -3
  43. omlish/inject/__init__.py +49 -17
  44. omlish/inject/binder.py +185 -5
  45. omlish/inject/bindings.py +3 -36
  46. omlish/inject/eagers.py +2 -8
  47. omlish/inject/elements.py +31 -10
  48. omlish/inject/exceptions.py +1 -1
  49. omlish/inject/impl/elements.py +37 -12
  50. omlish/inject/impl/injector.py +72 -25
  51. omlish/inject/impl/inspect.py +33 -5
  52. omlish/inject/impl/origins.py +77 -0
  53. omlish/inject/impl/{private.py → privates.py} +2 -2
  54. omlish/inject/impl/scopes.py +6 -2
  55. omlish/inject/injector.py +8 -4
  56. omlish/inject/inspect.py +18 -0
  57. omlish/inject/keys.py +8 -14
  58. omlish/inject/listeners.py +26 -0
  59. omlish/inject/managed.py +76 -10
  60. omlish/inject/multis.py +68 -18
  61. omlish/inject/origins.py +30 -0
  62. omlish/inject/overrides.py +5 -4
  63. omlish/inject/{private.py → privates.py} +6 -10
  64. omlish/inject/providers.py +12 -85
  65. omlish/inject/scopes.py +13 -6
  66. omlish/inject/types.py +3 -1
  67. omlish/inject/utils.py +18 -0
  68. omlish/iterators.py +69 -2
  69. omlish/lang/__init__.py +24 -9
  70. omlish/lang/cached.py +2 -2
  71. omlish/lang/classes/restrict.py +12 -1
  72. omlish/lang/classes/simple.py +18 -8
  73. omlish/lang/contextmanagers.py +13 -4
  74. omlish/lang/descriptors.py +132 -1
  75. omlish/lang/functions.py +8 -28
  76. omlish/lang/imports.py +67 -0
  77. omlish/lang/iterables.py +60 -1
  78. omlish/lang/maybes.py +3 -0
  79. omlish/lang/objects.py +38 -0
  80. omlish/lang/strings.py +25 -0
  81. omlish/lang/sys.py +9 -0
  82. omlish/lang/typing.py +42 -0
  83. omlish/lifecycles/__init__.py +34 -0
  84. omlish/lifecycles/abstract.py +43 -0
  85. omlish/lifecycles/base.py +51 -0
  86. omlish/lifecycles/contextmanagers.py +74 -0
  87. omlish/lifecycles/controller.py +116 -0
  88. omlish/lifecycles/manager.py +161 -0
  89. omlish/lifecycles/states.py +43 -0
  90. omlish/lifecycles/transitions.py +64 -0
  91. omlish/lite/__init__.py +1 -0
  92. omlish/lite/cached.py +18 -0
  93. omlish/lite/check.py +29 -0
  94. omlish/lite/contextmanagers.py +18 -0
  95. omlish/lite/json.py +30 -0
  96. omlish/lite/logs.py +52 -0
  97. omlish/lite/marshal.py +316 -0
  98. omlish/lite/reflect.py +49 -0
  99. omlish/lite/runtime.py +18 -0
  100. omlish/lite/secrets.py +19 -0
  101. omlish/lite/strings.py +25 -0
  102. omlish/lite/subprocesses.py +112 -0
  103. omlish/logs/configs.py +15 -2
  104. omlish/logs/formatters.py +7 -2
  105. omlish/marshal/__init__.py +32 -0
  106. omlish/marshal/any.py +5 -5
  107. omlish/marshal/base.py +27 -11
  108. omlish/marshal/base64.py +24 -9
  109. omlish/marshal/dataclasses.py +34 -28
  110. omlish/marshal/datetimes.py +74 -18
  111. omlish/marshal/enums.py +14 -8
  112. omlish/marshal/exceptions.py +11 -1
  113. omlish/marshal/factories.py +59 -74
  114. omlish/marshal/forbidden.py +35 -0
  115. omlish/marshal/global_.py +11 -4
  116. omlish/marshal/iterables.py +21 -24
  117. omlish/marshal/mappings.py +23 -26
  118. omlish/marshal/naming.py +4 -0
  119. omlish/marshal/numbers.py +51 -0
  120. omlish/marshal/objects.py +1 -0
  121. omlish/marshal/optionals.py +11 -12
  122. omlish/marshal/polymorphism.py +86 -21
  123. omlish/marshal/primitives.py +4 -5
  124. omlish/marshal/standard.py +13 -8
  125. omlish/marshal/uuids.py +4 -5
  126. omlish/matchfns.py +218 -0
  127. omlish/os.py +64 -0
  128. omlish/reflect/__init__.py +39 -0
  129. omlish/reflect/isinstance.py +38 -0
  130. omlish/reflect/ops.py +84 -0
  131. omlish/reflect/subst.py +110 -0
  132. omlish/reflect/types.py +275 -0
  133. omlish/secrets/__init__.py +23 -0
  134. omlish/secrets/crypto.py +132 -0
  135. omlish/secrets/marshal.py +70 -0
  136. omlish/secrets/openssl.py +207 -0
  137. omlish/secrets/passwords.py +120 -0
  138. omlish/secrets/secrets.py +299 -0
  139. omlish/secrets/subprocesses.py +42 -0
  140. omlish/sql/dbs.py +7 -6
  141. omlish/sql/duckdb.py +136 -0
  142. omlish/sql/exprs.py +12 -0
  143. omlish/sql/secrets.py +10 -0
  144. omlish/sql/sqlean.py +17 -0
  145. omlish/term.py +2 -2
  146. omlish/testing/pytest/__init__.py +3 -2
  147. omlish/testing/pytest/inject/harness.py +3 -3
  148. omlish/testing/pytest/marks.py +4 -7
  149. omlish/testing/pytest/plugins/__init__.py +1 -0
  150. omlish/testing/pytest/plugins/asyncs.py +136 -0
  151. omlish/testing/pytest/plugins/pydevd.py +1 -1
  152. omlish/testing/pytest/plugins/switches.py +54 -19
  153. omlish/text/glyphsplit.py +97 -0
  154. omlish-0.0.0.dev7.dist-info/METADATA +50 -0
  155. omlish-0.0.0.dev7.dist-info/RECORD +268 -0
  156. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev7.dist-info}/WHEEL +1 -1
  157. omlish/reflect.py +0 -355
  158. omlish-0.0.0.dev5.dist-info/METADATA +0 -34
  159. omlish-0.0.0.dev5.dist-info/RECORD +0 -212
  160. /omlish/{asyncs/futures.py → concurrent.py} +0 -0
  161. /omlish/{configs → formats}/props.py +0 -0
  162. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev7.dist-info}/LICENSE +0 -0
  163. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev7.dist-info}/top_level.txt +0 -0
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,21 +68,18 @@ 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,
81
74
  attr_property,
82
75
  classonly,
76
+ decorator,
83
77
  is_method_descriptor,
84
78
  item_property,
79
+ unwrap_func,
80
+ unwrap_func_with_partials,
85
81
  unwrap_method_descriptors,
82
+ update_wrapper_except_dict,
86
83
  )
87
84
 
88
85
  from .exceptions import ( # noqa
@@ -99,14 +96,14 @@ from .functions import ( # noqa
99
96
  is_lambda,
100
97
  is_none,
101
98
  is_not_none,
99
+ isinstance_of,
100
+ issubclass_of,
102
101
  maybe_call,
103
102
  periodically,
104
103
  raise_,
105
104
  raising,
106
105
  recurse,
107
106
  try_,
108
- unwrap_func,
109
- unwrap_func_with_partials,
110
107
  void,
111
108
  )
112
109
 
@@ -117,6 +114,7 @@ from .imports import ( # noqa
117
114
  import_module_attr,
118
115
  lazy_import,
119
116
  proxy_import,
117
+ resolve_import_name,
120
118
  try_import,
121
119
  yield_import_all,
122
120
  yield_importable,
@@ -124,11 +122,16 @@ from .imports import ( # noqa
124
122
 
125
123
  from .iterables import ( # noqa
126
124
  BUILTIN_SCALAR_ITERABLE_TYPES,
125
+ Generator,
127
126
  asrange,
128
127
  exhaust,
128
+ flatmap,
129
+ flatten,
129
130
  ilen,
131
+ itergen,
130
132
  peek,
131
133
  prodrange,
134
+ renumerate,
132
135
  take,
133
136
  )
134
137
 
@@ -143,12 +146,18 @@ from .objects import ( # noqa
143
146
  SimpleProxy,
144
147
  arg_repr,
145
148
  attr_repr,
149
+ can_weakref,
150
+ deep_subclasses,
146
151
  new_type,
147
152
  opt_repr,
148
153
  super_meta,
149
154
  )
150
155
 
151
156
  from .strings import ( # noqa
157
+ BOOL_FALSE_STRINGS,
158
+ BOOL_STRINGS,
159
+ BOOL_TRUE_STRINGS,
160
+ STRING_BOOL_VALUES,
152
161
  camel_case,
153
162
  indent_lines,
154
163
  is_dunder,
@@ -161,6 +170,8 @@ from .strings import ( # noqa
161
170
  )
162
171
 
163
172
  from .sys import ( # noqa
173
+ REQUIRED_PYTHON_VERSION,
174
+ check_runtime_version,
164
175
  is_gil_enabled,
165
176
  )
166
177
 
@@ -174,6 +185,10 @@ from .timeouts import ( # noqa
174
185
 
175
186
  from .typing import ( # noqa
176
187
  BytesLike,
188
+ Func0,
189
+ Func1,
190
+ Func2,
191
+ Func3,
177
192
  protocol_check,
178
193
  typed_lambda,
179
194
  typed_partial,
omlish/lang/cached.py CHANGED
@@ -11,8 +11,8 @@ import typing as ta
11
11
 
12
12
  from .contextmanagers import DefaultLockable
13
13
  from .contextmanagers import default_lock
14
- from .functions import unwrap_func
15
- from .functions import unwrap_func_with_partials
14
+ from .descriptors import unwrap_func
15
+ from .descriptors import unwrap_func_with_partials
16
16
 
17
17
 
18
18
  P = ta.ParamSpec('P')
@@ -78,7 +78,8 @@ class PackageSealed:
78
78
  for base in cls.__bases__:
79
79
  if base is not Abstract:
80
80
  if PackageSealed in base.__bases__:
81
- if cls.__module__.split('.')[:-1] != base.__module__.split('.')[:-1]:
81
+ pfx = base.__module__.split('.')[:-1]
82
+ if cls.__module__.split('.')[:len(pfx)] != pfx:
82
83
  raise SealedError(base)
83
84
  super().__init_subclass__(**kwargs)
84
85
 
@@ -96,9 +97,19 @@ class NotInstantiable(Abstract):
96
97
  class NotPicklable:
97
98
  __slots__ = ()
98
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
99
109
  def __getstate__(self) -> ta.NoReturn:
100
110
  raise TypeError
101
111
 
112
+ @ta.final
102
113
  def __setstate__(self, state) -> ta.NoReturn:
103
114
  raise TypeError
104
115
 
@@ -14,13 +14,25 @@ V = ta.TypeVar('V')
14
14
  ##
15
15
 
16
16
 
17
- class _Namespace(Final):
17
+ class _NamespaceMeta(abc.ABCMeta):
18
+ def __new__(mcls, name, bases, namespace):
19
+ if bases:
20
+ for nc in (NotInstantiable,):
21
+ if nc not in bases:
22
+ bases += (nc,)
23
+ return super().__new__(mcls, name, bases, namespace)
18
24
 
19
- def __mro_entries__(self, bases, **kwargs):
20
- return (Final, NotInstantiable)
25
+ def __iter__(cls) -> ta.Iterator[tuple[str, ta.Any]]:
26
+ for a in dir(cls):
27
+ if not a.startswith('_'):
28
+ yield (a, getattr(cls, a))
21
29
 
30
+ def __getitem__(cls, n: str) -> ta.Any:
31
+ return getattr(cls, n)
22
32
 
23
- Namespace: type = _Namespace() # type: ignore
33
+
34
+ class Namespace(metaclass=_NamespaceMeta):
35
+ pass
24
36
 
25
37
 
26
38
  ##
@@ -54,8 +66,6 @@ class _MarkerMeta(abc.ABCMeta):
54
66
  class Marker(NotInstantiable, metaclass=_MarkerMeta):
55
67
  """A marker."""
56
68
 
57
- __slots__ = ()
58
-
59
69
 
60
70
  ##
61
71
 
@@ -77,7 +87,7 @@ _SINGLETON_LOCK = threading.RLock()
77
87
 
78
88
 
79
89
  def _set_singleton_instance(inst):
80
- cls = type(inst)
90
+ cls = inst.__class__
81
91
  if _SINGLETON_INSTANCE_ATTR in cls.__dict__:
82
92
  raise TypeError(cls)
83
93
 
@@ -86,7 +96,7 @@ def _set_singleton_instance(inst):
86
96
 
87
97
  @functools.wraps(old_init)
88
98
  def new_init(self):
89
- if type(self) is not cls:
99
+ if self.__class__ is not cls:
90
100
  old_init(self) # noqa
91
101
 
92
102
  setattr(cls, '__init__', new_init)
@@ -1,3 +1,7 @@
1
+ """
2
+ TODO:
3
+ - AsyncExitStacked
4
+ """
1
5
  import abc
2
6
  import contextlib
3
7
  import contextvars
@@ -15,7 +19,7 @@ T = ta.TypeVar('T')
15
19
 
16
20
  class ContextManaged:
17
21
 
18
- def __enter__(self: ta.Self) -> ta.Self:
22
+ def __enter__(self) -> ta.Self:
19
23
  return self
20
24
 
21
25
  def __exit__(
@@ -77,7 +81,7 @@ class ContextManager(abc.ABC, ta.Generic[T]):
77
81
  exc_type: type[BaseException] | None,
78
82
  exc_val: BaseException | None,
79
83
  exc_tb: types.TracebackType | None,
80
- ) -> None:
84
+ ) -> bool | None:
81
85
  return self._contextmanager.__exit__(exc_type, exc_val, exc_tb)
82
86
 
83
87
 
@@ -198,7 +202,7 @@ class ExitStacked:
198
202
  def _enter_context(self, context_manager: ta.ContextManager[T]) -> T:
199
203
  return self._exit_stack.enter_context(ta.cast(ta.ContextManager, context_manager))
200
204
 
201
- def __enter__(self: ta.Self) -> ta.Self:
205
+ def __enter__(self) -> ta.Self:
202
206
  try:
203
207
  superfn = super().__enter__ # type: ignore
204
208
  except AttributeError:
@@ -303,17 +307,22 @@ Lockable = ta.Callable[[], ta.ContextManager]
303
307
  DefaultLockable = bool | Lockable | ta.ContextManager | None
304
308
 
305
309
 
306
- def default_lock(value: DefaultLockable, default: DefaultLockable) -> Lockable:
310
+ def default_lock(value: DefaultLockable, default: DefaultLockable = None) -> Lockable:
307
311
  if value is None:
308
312
  value = default
313
+
309
314
  if value is True:
310
315
  lock = threading.RLock()
311
316
  return lambda: lock
317
+
312
318
  elif value is False or value is None:
313
319
  return NOP_CONTEXT_MANAGER
320
+
314
321
  elif callable(value):
315
322
  return value
323
+
316
324
  elif isinstance(value, ta.ContextManager):
317
325
  return lambda: value
326
+
318
327
  else:
319
328
  raise TypeError(value)
@@ -1,9 +1,11 @@
1
1
  import functools
2
2
  import operator
3
+ import types
3
4
  import typing as ta
4
5
 
5
6
 
6
7
  T = ta.TypeVar('T')
8
+ P = ta.ParamSpec('P')
7
9
 
8
10
 
9
11
  ##
@@ -31,6 +33,19 @@ def is_method_descriptor(obj: ta.Any) -> bool:
31
33
  return isinstance(obj, (*BUILTIN_METHOD_DESCRIPTORS, _MethodDescriptor))
32
34
 
33
35
 
36
+ def _has_method_descriptor(obj: ta.Any) -> bool:
37
+ while True:
38
+ if is_method_descriptor(obj):
39
+ return True
40
+ elif isinstance(obj, functools.partial):
41
+ obj = obj.func
42
+ else:
43
+ try:
44
+ obj = getattr(obj, '__wrapped__')
45
+ except AttributeError:
46
+ return False
47
+
48
+
34
49
  def unwrap_method_descriptors(fn: ta.Callable) -> ta.Callable:
35
50
  while is_method_descriptor(fn):
36
51
  fn = fn.__func__ # type: ignore # noqa
@@ -40,6 +55,122 @@ def unwrap_method_descriptors(fn: ta.Callable) -> ta.Callable:
40
55
  ##
41
56
 
42
57
 
58
+ def unwrap_func(fn: ta.Callable) -> ta.Callable:
59
+ fn, _ = unwrap_func_with_partials(fn)
60
+ return fn
61
+
62
+
63
+ def unwrap_func_with_partials(fn: ta.Callable) -> tuple[ta.Callable, list[functools.partial]]:
64
+ ps = []
65
+ while True:
66
+ if is_method_descriptor(fn) or isinstance(fn, types.MethodType):
67
+ fn = fn.__func__ # type: ignore
68
+ elif hasattr(fn, '__wrapped__'):
69
+ nxt = fn.__wrapped__
70
+ if not callable(nxt):
71
+ raise TypeError(nxt)
72
+ if nxt is fn:
73
+ raise TypeError(fn)
74
+ fn = nxt
75
+ # NOTE: __wrapped__ takes precedence - a partial might point to a bound Method when the important information is
76
+ # still the unbound func. see _decorator_descriptor for an example of this.
77
+ elif isinstance(fn, functools.partial):
78
+ ps.append(fn)
79
+ fn = fn.func
80
+ else:
81
+ break
82
+ return fn, ps
83
+
84
+
85
+ ##
86
+
87
+
88
+ WRAPPER_UPDATES_EXCEPT_DICT = tuple(a for a in functools.WRAPPER_UPDATES if a != '__dict__')
89
+
90
+
91
+ def update_wrapper_except_dict(
92
+ wrapper,
93
+ wrapped,
94
+ assigned=functools.WRAPPER_ASSIGNMENTS,
95
+ updated=WRAPPER_UPDATES_EXCEPT_DICT,
96
+ ):
97
+ return functools.update_wrapper(
98
+ wrapper,
99
+ wrapped,
100
+ assigned=assigned,
101
+ updated=updated,
102
+ )
103
+
104
+
105
+ ##
106
+
107
+
108
+ _DECORATOR_HANDLES_UNBOUND_METHODS = True
109
+
110
+
111
+ class _decorator_descriptor: # noqa
112
+ if not _DECORATOR_HANDLES_UNBOUND_METHODS:
113
+ def __init__(self, wrapper, fn):
114
+ self._wrapper, self._fn = wrapper, fn
115
+ update_wrapper_except_dict(self, fn)
116
+
117
+ def __get__(self, instance, owner=None):
118
+ return functools.update_wrapper(functools.partial(self._wrapper, fn := self._fn.__get__(instance, owner)), fn) # noqa
119
+
120
+ else:
121
+ def __init__(self, wrapper, fn):
122
+ self._wrapper, self._fn = wrapper, fn
123
+ self._md = _has_method_descriptor(fn)
124
+ update_wrapper_except_dict(self, fn)
125
+
126
+ def __get__(self, instance, owner=None):
127
+ fn = self._fn.__get__(instance, owner)
128
+ if self._md or instance is not None:
129
+ @functools.wraps(fn)
130
+ def inner(*args, **kwargs):
131
+ return self._wrapper(fn, *args, **kwargs)
132
+ return inner
133
+ else:
134
+ @functools.wraps(fn)
135
+ def outer(this, *args, **kwargs):
136
+ @functools.wraps(self._fn)
137
+ def inner(*args2, **kwargs2):
138
+ return fn(this, *args2, **kwargs2)
139
+ return self._wrapper(inner, *args, **kwargs)
140
+ return outer
141
+
142
+ def __repr__(self):
143
+ return f'{self.__class__.__name__}<{self._wrapper}, {self._fn}>'
144
+
145
+ def __call__(self, *args, **kwargs):
146
+ return self._wrapper(self._fn, *args, **kwargs)
147
+
148
+
149
+ class _decorator: # noqa
150
+ def __init__(self, wrapper):
151
+ self._wrapper = wrapper
152
+ update_wrapper_except_dict(self, wrapper)
153
+
154
+ def __repr__(self):
155
+ return f'{self.__class__.__name__}<{self._wrapper}>'
156
+
157
+ def __call__(self, fn):
158
+ return _decorator_descriptor(self._wrapper, fn) # noqa
159
+
160
+
161
+ # FIXME:
162
+ # def decorator(
163
+ # wrapper: ta.Callable[ta.Concatenate[ta.Any, P], T], # FIXME: https://youtrack.jetbrains.com/issue/PY-72164 # noqa
164
+ # ) -> ta.Callable[[ta.Callable[P, T]], ta.Callable[P, T]]:
165
+ # return _decorator(wrapper)
166
+
167
+
168
+ decorator = _decorator
169
+
170
+
171
+ ##
172
+
173
+
43
174
  class AccessForbiddenError(Exception):
44
175
 
45
176
  def __init__(self, name: str | None = None, *args: ta.Any, **kwargs: ta.Any) -> None:
@@ -87,7 +218,7 @@ class _ClassOnly:
87
218
  def _mth(self, x):
88
219
  self.__mth = x
89
220
 
90
- def __get__(self, instance, owner):
221
+ def __get__(self, instance, owner=None):
91
222
  if instance is not None:
92
223
  raise TypeError(f'method must not be used on instance: {self._mth}')
93
224
  return self._mth[0].__get__(instance, owner)
omlish/lang/functions.py CHANGED
@@ -3,8 +3,6 @@ import functools
3
3
  import time
4
4
  import typing as ta
5
5
 
6
- from .descriptors import is_method_descriptor
7
-
8
6
 
9
7
  T = ta.TypeVar('T')
10
8
  P = ta.ParamSpec('P')
@@ -41,32 +39,6 @@ def recurse(fn: ta.Callable[..., T], *args, **kwargs) -> T:
41
39
  ##
42
40
 
43
41
 
44
- def unwrap_func(fn: ta.Callable) -> ta.Callable:
45
- fn, _ = unwrap_func_with_partials(fn)
46
- return fn
47
-
48
-
49
- def unwrap_func_with_partials(fn: ta.Callable) -> tuple[ta.Callable, list[functools.partial]]:
50
- ps = []
51
- while True:
52
- if is_method_descriptor(fn):
53
- fn = fn.__func__ # type: ignore
54
- elif isinstance(fn, functools.partial):
55
- ps.append(fn)
56
- fn = fn.func
57
- else:
58
- nxt = getattr(fn, '__wrapped__', None)
59
- if not callable(nxt):
60
- break
61
- if nxt is fn:
62
- raise TypeError(fn)
63
- fn = nxt
64
- return fn, ps
65
-
66
-
67
- ##
68
-
69
-
70
42
  def raise_(o: BaseException) -> ta.NoReturn:
71
43
  raise o
72
44
 
@@ -128,6 +100,14 @@ def is_not_none(o: ta.Any) -> bool:
128
100
  return o is not None
129
101
 
130
102
 
103
+ def isinstance_of(class_or_tuple: ta.Any) -> ta.Callable[[ta.Any], bool]:
104
+ return lambda o: isinstance(o, class_or_tuple)
105
+
106
+
107
+ def issubclass_of(class_or_tuple: ta.Any) -> ta.Callable[[ta.Any], bool]:
108
+ return lambda o: issubclass(o, class_or_tuple)
109
+
110
+
131
111
  class VoidError(Exception):
132
112
  pass
133
113
 
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)