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/matchfns.py ADDED
@@ -0,0 +1,218 @@
1
+ import abc
2
+ import dataclasses as dc
3
+ import typing as ta
4
+
5
+ from . import lang
6
+
7
+
8
+ T = ta.TypeVar('T')
9
+ P = ta.ParamSpec('P')
10
+
11
+
12
+ ##
13
+
14
+
15
+ class MatchGuardError(Exception):
16
+ pass
17
+
18
+
19
+ class MatchFn(abc.ABC, ta.Generic[P, T]):
20
+ @abc.abstractmethod
21
+ def guard(self, *args: P.args, **kwargs: P.kwargs) -> bool:
22
+ raise NotImplementedError
23
+
24
+ @abc.abstractmethod
25
+ def fn(self, *args: P.args, **kwargs: P.kwargs) -> T:
26
+ raise NotImplementedError
27
+
28
+ def __get__(self, instance, owner=None):
29
+ return self
30
+
31
+ @ta.final
32
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
33
+ if not self.guard(*args, **kwargs):
34
+ raise MatchGuardError(*args, **kwargs)
35
+ return self.fn(*args, **kwargs)
36
+
37
+
38
+ ##
39
+
40
+
41
+ @dc.dataclass(frozen=True)
42
+ class SimpleMatchFn(MatchFn[P, T]):
43
+ _guard: ta.Callable[P, bool]
44
+ _fn: ta.Callable[P, T]
45
+
46
+ def guard(self, *args: P.args, **kwargs: P.kwargs) -> bool:
47
+ return self._guard(*args, **kwargs)
48
+
49
+ def fn(self, *args: P.args, **kwargs: P.kwargs) -> T:
50
+ return self._fn(*args, **kwargs)
51
+
52
+ def __get__(self, instance, owner=None):
53
+ return self.__class__(
54
+ self._guard.__get__(instance, owner), # noqa
55
+ self._fn.__get__(instance, owner), # noqa
56
+ )
57
+
58
+
59
+ @ta.overload
60
+ def simple(guard: ta.Callable[..., bool], fn: ta.Callable[P, T]) -> SimpleMatchFn[P, T]:
61
+ ...
62
+
63
+
64
+ @ta.overload
65
+ def simple(guard: ta.Callable[..., bool]) -> ta.Callable[[ta.Callable[P, T]], SimpleMatchFn[P, T]]:
66
+ ...
67
+
68
+
69
+ def simple(guard, fn=None):
70
+ def inner(fn): # noqa
71
+ return SimpleMatchFn(guard, fn)
72
+ if fn is not None:
73
+ return inner(fn)
74
+ else:
75
+ return inner
76
+
77
+
78
+ ##
79
+
80
+
81
+ class AmbiguousMatchesError(Exception):
82
+ pass
83
+
84
+
85
+ @dc.dataclass(frozen=True)
86
+ class MultiMatchFn(MatchFn[P, T]):
87
+ children: ta.Sequence[MatchFn[P, T]]
88
+ strict: bool = False
89
+
90
+ def _match(self, *args: P.args, **kwargs: P.kwargs) -> MatchFn[P, T] | None:
91
+ matches = []
92
+ for cur in self.children:
93
+ if cur.guard(*args, **kwargs):
94
+ if self.strict:
95
+ matches.append(cur)
96
+ else:
97
+ return cur
98
+ if not matches:
99
+ return None
100
+ elif len(matches) > 1:
101
+ raise AmbiguousMatchesError
102
+ else:
103
+ return matches[0]
104
+
105
+ def guard(self, *args: P.args, **kwargs: P.kwargs) -> bool:
106
+ return self._match(*args, **kwargs) is not None
107
+
108
+ def fn(self, *args: P.args, **kwargs: P.kwargs) -> T:
109
+ if (m := self._match(*args, **kwargs)) is None:
110
+ raise MatchGuardError(*args, **kwargs)
111
+ return m.fn(*args, **kwargs)
112
+
113
+ def __get__(self, instance, owner=None):
114
+ return self.__class__(
115
+ [c.__get__(instance, owner) for c in self.children],
116
+ strict=self.strict,
117
+ )
118
+
119
+
120
+ def multi(*children: MatchFn[P, T], strict: bool = False):
121
+ return MultiMatchFn(children, strict=strict) # noqa
122
+
123
+
124
+ ##
125
+
126
+
127
+ class CachedMultiFn(MatchFn[P, T]):
128
+ @staticmethod
129
+ def _default_key(*args, **kwargs):
130
+ return (args, tuple(sorted(kwargs.items(), key=lambda t: t[0])))
131
+
132
+ def __init__(
133
+ self,
134
+ f: MatchFn[P, T],
135
+ *,
136
+ key: ta.Callable[P, ta.Any] = _default_key,
137
+ lock: lang.DefaultLockable = None,
138
+ ) -> None:
139
+ super().__init__()
140
+ self._f = f
141
+ self._key = key
142
+ self._lock = lock
143
+ self._lock_impl = lang.default_lock(lock)()
144
+ self._dct: dict[ta.Any, lang.Maybe[ta.Any]] = {}
145
+
146
+ def guard(self, *args: P.args, **kwargs: P.kwargs) -> bool:
147
+ with self._lock_impl:
148
+ k = self._key(*args, **kwargs)
149
+ try:
150
+ e = self._dct[k]
151
+ except KeyError:
152
+ if self._f.guard(*args, **kwargs):
153
+ return True
154
+ else:
155
+ self._dct[k] = lang.empty()
156
+ return False
157
+ else:
158
+ return e.present
159
+
160
+ def fn(self, *args: P.args, **kwargs: P.kwargs) -> T:
161
+ with self._lock_impl:
162
+ k = self._key(*args, **kwargs)
163
+ try:
164
+ e = self._dct[k]
165
+ except KeyError:
166
+ try:
167
+ ret = self._f(*args, **kwargs)
168
+ except MatchGuardError:
169
+ self._dct[k] = lang.empty()
170
+ raise
171
+ else:
172
+ self._dct[k] = lang.just(ret)
173
+ return ret
174
+ else:
175
+ if e.present:
176
+ return e.must()
177
+ else:
178
+ raise MatchGuardError(*args, **kwargs)
179
+
180
+ def __get__(self, instance, owner=None):
181
+ return self.__class__(self._f.__get__(instance, owner), key=self._key) # noqa
182
+
183
+
184
+ cached = CachedMultiFn
185
+
186
+
187
+ ##
188
+
189
+
190
+ class MatchFnClass(MatchFn[P, T]):
191
+ _cls_match_fn: ta.ClassVar[MultiMatchFn]
192
+
193
+ def __init__(self) -> None:
194
+ super().__init__()
195
+ self.__match_fn: MatchFn[P, T] | None = None
196
+
197
+ @property
198
+ def _match_fn(self) -> MatchFn[P, T]:
199
+ if (mf := self.__match_fn) is None:
200
+ mf = self.__match_fn = self._cls_match_fn.__get__(self)
201
+ return mf
202
+
203
+ def __init_subclass__(cls, strict: bool = False, **kwargs):
204
+ super().__init_subclass__()
205
+ if '_cls_match_fn' in cls.__dict__:
206
+ raise AttributeError('_cls_match_fn')
207
+ d = {}
208
+ for c in cls.__mro__:
209
+ for a, o in c.__dict__.items():
210
+ if isinstance(o, MatchFn) and a not in d:
211
+ d[a] = o
212
+ cls._cls_match_fn = MultiMatchFn(list(d.values()), strict=strict)
213
+
214
+ def guard(self, *args: P.args, **kwargs: P.kwargs) -> bool:
215
+ return self._match_fn.guard(*args, **kwargs)
216
+
217
+ def fn(self, *args: P.args, **kwargs: P.kwargs) -> T:
218
+ return self._match_fn.fn(*args, **kwargs)
omlish/os.py CHANGED
@@ -1,6 +1,9 @@
1
1
  import contextlib
2
+ import fcntl
3
+ import os
2
4
  import resource
3
5
  import shutil
6
+ import signal
4
7
  import tempfile
5
8
  import typing as ta
6
9
 
@@ -39,3 +42,64 @@ def tmp_file(
39
42
  finally:
40
43
  if cleanup:
41
44
  shutil.rmtree(f.name, ignore_errors=True)
45
+
46
+
47
+ class Pidfile:
48
+ def __init__(self, path: str) -> None:
49
+ super().__init__()
50
+ self._path = path
51
+
52
+ _f: ta.TextIO
53
+
54
+ def __repr__(self) -> str:
55
+ return f'{self.__class__.__name__}({self._path!r})'
56
+
57
+ def __enter__(self) -> ta.Self:
58
+ fd = os.open(self._path, os.O_RDWR | os.O_CREAT, 0o600)
59
+ try:
60
+ os.set_inheritable(fd, True)
61
+ f = os.fdopen(fd, 'r+')
62
+ except Exception:
63
+ try:
64
+ os.close(fd)
65
+ except Exception: # noqa
66
+ pass
67
+ raise
68
+ self._f = f
69
+ return self
70
+
71
+ def __exit__(self, exc_type, exc_val, exc_tb):
72
+ if self._f is not None:
73
+ self._f.close()
74
+ del self._f
75
+
76
+ def try_lock(self) -> bool:
77
+ try:
78
+ fcntl.flock(self._f, fcntl.LOCK_EX | fcntl.LOCK_NB)
79
+ return True
80
+ except OSError:
81
+ return False
82
+
83
+ def write(self, pid: int | None = None) -> None:
84
+ if not self.try_lock():
85
+ raise RuntimeError('Could not get lock')
86
+ if pid is None:
87
+ pid = os.getpid()
88
+ self._f.write(f'{pid}\n')
89
+ self._f.flush()
90
+
91
+ def clear(self) -> None:
92
+ if not self.try_lock():
93
+ raise RuntimeError('Could not get lock')
94
+ self._f.seek(0)
95
+ self._f.truncate()
96
+
97
+ def read(self) -> int:
98
+ if self.try_lock():
99
+ raise RuntimeError('Got lock')
100
+ self._f.seek(0)
101
+ return int(self._f.read())
102
+
103
+ def kill(self, sig: int = signal.SIGTERM) -> None:
104
+ pid = self.read()
105
+ os.kill(pid, sig) # Still racy
@@ -0,0 +1,39 @@
1
+ from .types import ( # noqa
2
+ ANY,
3
+ Annotated,
4
+ Any,
5
+ Generic,
6
+ NewType,
7
+ TYPES,
8
+ Type,
9
+ Union,
10
+ get_orig_class,
11
+ get_params,
12
+ is_type,
13
+ is_union_type,
14
+ type_,
15
+ )
16
+
17
+ from .ops import ( # noqa
18
+ get_concrete_type,
19
+ get_underlying,
20
+ strip_annotations,
21
+ strip_objs,
22
+ to_annotation,
23
+ types_equivalent,
24
+ )
25
+
26
+ from .isinstance import ( # noqa
27
+ KNOWN_ISINSTANCE_GENERICS,
28
+ isinstance_of,
29
+ )
30
+
31
+ from .subst import ( # noqa
32
+ ALIAS_UPDATING_GENERIC_SUBSTITUTION,
33
+ DEFAULT_GENERIC_SUBSTITUTION,
34
+ GenericSubstitution,
35
+ generic_mro,
36
+ get_generic_bases,
37
+ get_type_var_replacements,
38
+ replace_type_vars,
39
+ )
@@ -0,0 +1,38 @@
1
+ import collections.abc
2
+ import typing as ta
3
+
4
+ from .ops import get_underlying
5
+ from .types import Generic
6
+ from .types import NewType
7
+ from .types import Type
8
+ from .types import Union
9
+
10
+
11
+ KNOWN_ISINSTANCE_GENERICS: ta.AbstractSet[type] = frozenset([
12
+ collections.abc.Mapping,
13
+ collections.abc.Sequence,
14
+ collections.abc.Set,
15
+ ])
16
+
17
+
18
+ def isinstance_of(rfl: Type) -> ta.Callable[[ta.Any], bool]:
19
+ if isinstance(rfl, type):
20
+ return lambda o: isinstance(o, rfl)
21
+
22
+ if isinstance(rfl, NewType):
23
+ return isinstance_of(get_underlying(rfl))
24
+
25
+ if isinstance(rfl, Union):
26
+ fns = [isinstance_of(a) for a in rfl.args]
27
+ return lambda o: any(fn(o) for fn in fns)
28
+
29
+ if isinstance(rfl, Generic):
30
+ if rfl.cls in (collections.abc.Sequence, collections.abc.Set):
31
+ [efn] = map(isinstance_of, rfl.args)
32
+ return lambda o: isinstance(o, rfl.cls) and all(efn(e) for e in o) # type: ignore
33
+
34
+ if rfl.cls == collections.abc.Mapping:
35
+ kfn, vfn = map(isinstance_of, rfl.args)
36
+ return lambda o: isinstance(o, rfl.cls) and all(kfn(k) and vfn(v) for k, v in o.items()) # type: ignore
37
+
38
+ raise TypeError(rfl)
omlish/reflect/ops.py ADDED
@@ -0,0 +1,84 @@
1
+ import typing as ta
2
+
3
+ from .types import Annotated
4
+ from .types import Any
5
+ from .types import Generic
6
+ from .types import NewType
7
+ from .types import Type
8
+ from .types import Union
9
+ from .types import type_
10
+
11
+
12
+ def strip_objs(ty: Type) -> Type:
13
+ if isinstance(ty, (type, ta.TypeVar, NewType, Any)):
14
+ return ty
15
+
16
+ if isinstance(ty, Union):
17
+ return Union(frozenset(map(strip_objs, ty.args)))
18
+
19
+ if isinstance(ty, Generic):
20
+ return Generic(ty.cls, tuple(map(strip_objs, ty.args)), ty.params, None)
21
+
22
+ if isinstance(ty, Annotated):
23
+ return Annotated(strip_objs(ty.ty), ty.md, None)
24
+
25
+ raise TypeError(ty)
26
+
27
+
28
+ def strip_annotations(ty: Type) -> Type:
29
+ if isinstance(ty, (type, ta.TypeVar, NewType, Any)):
30
+ return ty
31
+
32
+ if isinstance(ty, Union):
33
+ return Union(frozenset(map(strip_annotations, ty.args)))
34
+
35
+ if isinstance(ty, Generic):
36
+ return Generic(ty.cls, tuple(map(strip_annotations, ty.args)), ty.params, ty.obj)
37
+
38
+ if isinstance(ty, Annotated):
39
+ return strip_annotations(ty.ty)
40
+
41
+ raise TypeError(ty)
42
+
43
+
44
+ def types_equivalent(l: Type, r: Type) -> bool:
45
+ if isinstance(l, Generic) and isinstance(r, Generic):
46
+ return l.cls == r.cls and l.args == r.args
47
+
48
+ return l == r
49
+
50
+
51
+ def get_underlying(nt: NewType) -> Type:
52
+ return type_(nt.obj.__supertype__) # noqa
53
+
54
+
55
+ def get_concrete_type(ty: Type) -> type | None:
56
+ if isinstance(ty, type):
57
+ return ty
58
+
59
+ if isinstance(ty, Generic):
60
+ return ty.cls
61
+
62
+ if isinstance(ty, NewType):
63
+ return get_concrete_type(get_underlying(ty))
64
+
65
+ if isinstance(ty, (Union, ta.TypeVar, Any)):
66
+ return None
67
+
68
+ raise TypeError(ty)
69
+
70
+
71
+ def to_annotation(ty: Type) -> ta.Any:
72
+ if isinstance(ty, Generic):
73
+ return ty.obj if ty.obj is not None else ty.cls
74
+
75
+ if isinstance(ty, Union):
76
+ return ta.Union[*tuple(to_annotation(e) for e in ty.args)]
77
+
78
+ if isinstance(ty, (type, ta.TypeVar, NewType)):
79
+ return ty
80
+
81
+ if isinstance(ty, Any):
82
+ return ta.Any
83
+
84
+ raise TypeError(ty)
@@ -0,0 +1,110 @@
1
+ import dataclasses as dc
2
+ import types
3
+ import typing as ta
4
+
5
+ from .. import c3
6
+ from .. import lang
7
+ from .ops import get_concrete_type
8
+ from .ops import to_annotation
9
+ from .types import Any
10
+ from .types import Generic
11
+ from .types import NewType
12
+ from .types import Type
13
+ from .types import Union
14
+ from .types import get_params
15
+ from .types import type_
16
+
17
+
18
+ if ta.TYPE_CHECKING:
19
+ from ..collections import cache
20
+ else:
21
+ cache = lang.proxy_import('.collections.cache', __package__)
22
+
23
+
24
+ def get_type_var_replacements(ty: Type) -> ta.Mapping[ta.TypeVar, Type]:
25
+ if isinstance(ty, Generic):
26
+ return dict(zip(ty.params, ty.args))
27
+
28
+ return {}
29
+
30
+
31
+ def replace_type_vars(
32
+ ty: Type,
33
+ rpl: ta.Mapping[ta.TypeVar, Type],
34
+ *,
35
+ update_aliases: bool = False,
36
+ ) -> Type:
37
+ def rec(cur):
38
+ if isinstance(cur, (type, NewType, Any)):
39
+ return cur
40
+
41
+ if isinstance(cur, Generic):
42
+ args = tuple(rec(a) for a in cur.args)
43
+ if update_aliases:
44
+ obj = cur.obj
45
+ if (ops := get_params(obj)):
46
+ nargs = [to_annotation(rpl[p]) for p in ops]
47
+ if ta.get_origin(obj) is ta.Generic:
48
+ # FIXME: None? filter_typing_generic in get_generic_bases?
49
+ pass
50
+ else:
51
+ obj = cur.obj[*nargs]
52
+ else:
53
+ obj = None
54
+ return dc.replace(cur, args=args, obj=obj)
55
+
56
+ if isinstance(cur, Union):
57
+ return Union(frozenset(rec(e) for e in cur.args))
58
+
59
+ if isinstance(cur, ta.TypeVar):
60
+ return rpl[cur]
61
+
62
+ raise TypeError(cur)
63
+
64
+ return rec(ty)
65
+
66
+
67
+ class GenericSubstitution:
68
+ def __init__(
69
+ self,
70
+ *,
71
+ update_aliases: bool = False,
72
+ cache_size: int = 0, # FIXME: ta.Generic isn't weakrefable..
73
+ ) -> None:
74
+ super().__init__()
75
+
76
+ self._update_aliases = update_aliases
77
+
78
+ if cache_size > 0:
79
+ self.get_generic_bases = cache.cache(weak_keys=True, max_size=cache_size)(self.get_generic_bases) # type: ignore # noqa
80
+ self.generic_mro = cache.cache(weak_keys=True, max_size=cache_size)(self.generic_mro) # type: ignore
81
+
82
+ def get_generic_bases(self, ty: Type) -> tuple[Type, ...]:
83
+ if (cty := get_concrete_type(ty)) is not None:
84
+ rpl = get_type_var_replacements(ty)
85
+ ret: list[Type] = []
86
+ for b in types.get_original_bases(cty):
87
+ bty = type_(b)
88
+ if isinstance(bty, Generic) and isinstance(b, type):
89
+ # FIXME: throws away relative types, but can't use original vars as they're class-contextual
90
+ bty = type_(b[*((ta.Any,) * len(bty.params))]) # type: ignore
91
+ rty = replace_type_vars(bty, rpl, update_aliases=self._update_aliases)
92
+ ret.append(rty)
93
+ return tuple(ret)
94
+ return ()
95
+
96
+ def generic_mro(self, obj: ta.Any) -> list[Type]:
97
+ mro = c3.mro(
98
+ type_(obj),
99
+ get_bases=lambda t: self.get_generic_bases(t),
100
+ is_subclass=lambda l, r: issubclass(get_concrete_type(l), get_concrete_type(r)), # type: ignore
101
+ )
102
+ return [ty for ty in mro if get_concrete_type(ty) is not ta.Generic]
103
+
104
+
105
+ DEFAULT_GENERIC_SUBSTITUTION = GenericSubstitution()
106
+
107
+ get_generic_bases = DEFAULT_GENERIC_SUBSTITUTION.get_generic_bases
108
+ generic_mro = DEFAULT_GENERIC_SUBSTITUTION.generic_mro
109
+
110
+ ALIAS_UPDATING_GENERIC_SUBSTITUTION = GenericSubstitution(update_aliases=True)