omlish 0.0.0.dev1__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.
- omlish/__about__.py +7 -0
- omlish/__init__.py +0 -0
- omlish/argparse.py +223 -0
- omlish/asyncs/__init__.py +17 -0
- omlish/asyncs/anyio.py +23 -0
- omlish/asyncs/asyncio.py +19 -0
- omlish/asyncs/asyncs.py +76 -0
- omlish/asyncs/futures.py +179 -0
- omlish/asyncs/trio.py +11 -0
- omlish/c3.py +173 -0
- omlish/cached.py +9 -0
- omlish/check.py +231 -0
- omlish/collections/__init__.py +63 -0
- omlish/collections/_abc.py +156 -0
- omlish/collections/_io_abc.py +78 -0
- omlish/collections/cache/__init__.py +11 -0
- omlish/collections/cache/descriptor.py +188 -0
- omlish/collections/cache/impl.py +485 -0
- omlish/collections/cache/types.py +37 -0
- omlish/collections/coerce.py +337 -0
- omlish/collections/frozen.py +148 -0
- omlish/collections/identity.py +106 -0
- omlish/collections/indexed.py +75 -0
- omlish/collections/mappings.py +127 -0
- omlish/collections/ordered.py +81 -0
- omlish/collections/persistent.py +36 -0
- omlish/collections/skiplist.py +193 -0
- omlish/collections/sorted.py +126 -0
- omlish/collections/treap.py +228 -0
- omlish/collections/treapmap.py +144 -0
- omlish/collections/unmodifiable.py +174 -0
- omlish/collections/utils.py +110 -0
- omlish/configs/__init__.py +0 -0
- omlish/configs/flattening.py +147 -0
- omlish/configs/props.py +64 -0
- omlish/dataclasses/__init__.py +83 -0
- omlish/dataclasses/impl/__init__.py +6 -0
- omlish/dataclasses/impl/api.py +260 -0
- omlish/dataclasses/impl/as_.py +76 -0
- omlish/dataclasses/impl/exceptions.py +2 -0
- omlish/dataclasses/impl/fields.py +148 -0
- omlish/dataclasses/impl/frozen.py +55 -0
- omlish/dataclasses/impl/hashing.py +85 -0
- omlish/dataclasses/impl/init.py +173 -0
- omlish/dataclasses/impl/internals.py +118 -0
- omlish/dataclasses/impl/main.py +150 -0
- omlish/dataclasses/impl/metaclass.py +126 -0
- omlish/dataclasses/impl/metadata.py +74 -0
- omlish/dataclasses/impl/order.py +47 -0
- omlish/dataclasses/impl/params.py +150 -0
- omlish/dataclasses/impl/processing.py +16 -0
- omlish/dataclasses/impl/reflect.py +173 -0
- omlish/dataclasses/impl/replace.py +40 -0
- omlish/dataclasses/impl/repr.py +34 -0
- omlish/dataclasses/impl/simple.py +92 -0
- omlish/dataclasses/impl/slots.py +80 -0
- omlish/dataclasses/impl/utils.py +167 -0
- omlish/defs.py +193 -0
- omlish/dispatch/__init__.py +3 -0
- omlish/dispatch/dispatch.py +137 -0
- omlish/dispatch/functions.py +52 -0
- omlish/dispatch/methods.py +162 -0
- omlish/docker.py +149 -0
- omlish/dynamic.py +220 -0
- omlish/graphs/__init__.py +0 -0
- omlish/graphs/dot/__init__.py +19 -0
- omlish/graphs/dot/items.py +162 -0
- omlish/graphs/dot/rendering.py +147 -0
- omlish/graphs/dot/utils.py +30 -0
- omlish/graphs/trees.py +249 -0
- omlish/http/__init__.py +0 -0
- omlish/http/consts.py +20 -0
- omlish/http/wsgi.py +34 -0
- omlish/inject/__init__.py +85 -0
- omlish/inject/binder.py +12 -0
- omlish/inject/bindings.py +49 -0
- omlish/inject/eagers.py +21 -0
- omlish/inject/elements.py +43 -0
- omlish/inject/exceptions.py +49 -0
- omlish/inject/impl/__init__.py +0 -0
- omlish/inject/impl/bindings.py +19 -0
- omlish/inject/impl/elements.py +154 -0
- omlish/inject/impl/injector.py +182 -0
- omlish/inject/impl/inspect.py +98 -0
- omlish/inject/impl/private.py +109 -0
- omlish/inject/impl/providers.py +132 -0
- omlish/inject/impl/scopes.py +198 -0
- omlish/inject/injector.py +40 -0
- omlish/inject/inspect.py +14 -0
- omlish/inject/keys.py +43 -0
- omlish/inject/managed.py +24 -0
- omlish/inject/overrides.py +18 -0
- omlish/inject/private.py +29 -0
- omlish/inject/providers.py +111 -0
- omlish/inject/proxy.py +48 -0
- omlish/inject/scopes.py +84 -0
- omlish/inject/types.py +21 -0
- omlish/iterators.py +184 -0
- omlish/json.py +194 -0
- omlish/lang/__init__.py +112 -0
- omlish/lang/cached.py +267 -0
- omlish/lang/classes/__init__.py +24 -0
- omlish/lang/classes/abstract.py +74 -0
- omlish/lang/classes/restrict.py +137 -0
- omlish/lang/classes/simple.py +120 -0
- omlish/lang/classes/test/__init__.py +0 -0
- omlish/lang/classes/test/test_abstract.py +89 -0
- omlish/lang/classes/test/test_restrict.py +71 -0
- omlish/lang/classes/test/test_simple.py +58 -0
- omlish/lang/classes/test/test_virtual.py +72 -0
- omlish/lang/classes/virtual.py +130 -0
- omlish/lang/clsdct.py +67 -0
- omlish/lang/cmp.py +63 -0
- omlish/lang/contextmanagers.py +249 -0
- omlish/lang/datetimes.py +67 -0
- omlish/lang/descriptors.py +52 -0
- omlish/lang/functions.py +126 -0
- omlish/lang/imports.py +153 -0
- omlish/lang/iterables.py +54 -0
- omlish/lang/maybes.py +136 -0
- omlish/lang/objects.py +103 -0
- omlish/lang/resolving.py +50 -0
- omlish/lang/strings.py +128 -0
- omlish/lang/typing.py +92 -0
- omlish/libc.py +532 -0
- omlish/logs/__init__.py +9 -0
- omlish/logs/_abc.py +247 -0
- omlish/logs/configs.py +62 -0
- omlish/logs/filters.py +9 -0
- omlish/logs/formatters.py +67 -0
- omlish/logs/utils.py +20 -0
- omlish/marshal/__init__.py +52 -0
- omlish/marshal/any.py +25 -0
- omlish/marshal/base.py +201 -0
- omlish/marshal/base64.py +25 -0
- omlish/marshal/dataclasses.py +115 -0
- omlish/marshal/datetimes.py +90 -0
- omlish/marshal/enums.py +43 -0
- omlish/marshal/exceptions.py +7 -0
- omlish/marshal/factories.py +129 -0
- omlish/marshal/global_.py +33 -0
- omlish/marshal/iterables.py +57 -0
- omlish/marshal/mappings.py +66 -0
- omlish/marshal/naming.py +17 -0
- omlish/marshal/objects.py +106 -0
- omlish/marshal/optionals.py +49 -0
- omlish/marshal/polymorphism.py +147 -0
- omlish/marshal/primitives.py +43 -0
- omlish/marshal/registries.py +57 -0
- omlish/marshal/standard.py +80 -0
- omlish/marshal/utils.py +23 -0
- omlish/marshal/uuids.py +29 -0
- omlish/marshal/values.py +30 -0
- omlish/math.py +184 -0
- omlish/os.py +32 -0
- omlish/reflect.py +359 -0
- omlish/replserver/__init__.py +5 -0
- omlish/replserver/__main__.py +4 -0
- omlish/replserver/console.py +247 -0
- omlish/replserver/server.py +146 -0
- omlish/runmodule.py +28 -0
- omlish/stats.py +342 -0
- omlish/term.py +222 -0
- omlish/testing/__init__.py +7 -0
- omlish/testing/pydevd.py +225 -0
- omlish/testing/pytest/__init__.py +8 -0
- omlish/testing/pytest/helpers.py +35 -0
- omlish/testing/pytest/inject/__init__.py +1 -0
- omlish/testing/pytest/inject/harness.py +159 -0
- omlish/testing/pytest/plugins/__init__.py +20 -0
- omlish/testing/pytest/plugins/_registry.py +6 -0
- omlish/testing/pytest/plugins/logging.py +13 -0
- omlish/testing/pytest/plugins/pycharm.py +54 -0
- omlish/testing/pytest/plugins/repeat.py +19 -0
- omlish/testing/pytest/plugins/skips.py +32 -0
- omlish/testing/pytest/plugins/spacing.py +19 -0
- omlish/testing/pytest/plugins/switches.py +70 -0
- omlish/testing/testing.py +102 -0
- omlish/text/__init__.py +0 -0
- omlish/text/delimit.py +171 -0
- omlish/text/indent.py +50 -0
- omlish/text/parts.py +265 -0
- omlish-0.0.0.dev1.dist-info/LICENSE +21 -0
- omlish-0.0.0.dev1.dist-info/METADATA +17 -0
- omlish-0.0.0.dev1.dist-info/RECORD +187 -0
- omlish-0.0.0.dev1.dist-info/WHEEL +5 -0
- omlish-0.0.0.dev1.dist-info/top_level.txt +1 -0
omlish/defs.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A self-awarely unpythonic set of helpers for defining common boilerplate methods (repr, hash_eq, delegates, etc) in
|
|
3
|
+
class definitions. Should be used sparingly for methods not directly used by humans (like repr) - @property's should
|
|
4
|
+
remain @property's for type annotation, tool assistance, debugging, and otherwise, but these are still nice to have in
|
|
5
|
+
certain circumstances (the real-world alternative usually being simply not adding them).
|
|
6
|
+
"""
|
|
7
|
+
import abc
|
|
8
|
+
import functools
|
|
9
|
+
import operator
|
|
10
|
+
import threading
|
|
11
|
+
|
|
12
|
+
from . import lang
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_repr = repr
|
|
16
|
+
|
|
17
|
+
BASICS = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _basic(fn):
|
|
21
|
+
if fn.__name__ in BASICS:
|
|
22
|
+
raise NameError(fn.__name__)
|
|
23
|
+
BASICS[fn.__name__] = fn
|
|
24
|
+
return fn
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@lang.cls_dct_fn()
|
|
28
|
+
def basic(cls_dct, *attrs, basics=None):
|
|
29
|
+
if basics is None:
|
|
30
|
+
basics = BASICS.keys()
|
|
31
|
+
for k in basics:
|
|
32
|
+
fn = BASICS[k]
|
|
33
|
+
fn(*attrs, cls_dct=cls_dct)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_REPR_SEEN = threading.local()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _repr_guard(fn):
|
|
40
|
+
@functools.wraps(fn)
|
|
41
|
+
def inner(obj, *args, **kwargs):
|
|
42
|
+
try:
|
|
43
|
+
ids = _REPR_SEEN.ids
|
|
44
|
+
except AttributeError:
|
|
45
|
+
ids = _REPR_SEEN.ids = set()
|
|
46
|
+
try:
|
|
47
|
+
ids.add(id(obj))
|
|
48
|
+
return fn(obj, *args, **kwargs)
|
|
49
|
+
finally:
|
|
50
|
+
del _REPR_SEEN.ids
|
|
51
|
+
else:
|
|
52
|
+
if id(obj) in ids:
|
|
53
|
+
return f'<seen:{type(obj).__name__}@{hex(id(obj))[2:]}>'
|
|
54
|
+
ids.add(id(obj))
|
|
55
|
+
return fn(obj, *args, **kwargs)
|
|
56
|
+
|
|
57
|
+
return inner
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@_repr_guard
|
|
61
|
+
def build_attr_repr(obj, *, mro=False):
|
|
62
|
+
if mro:
|
|
63
|
+
attrs = [
|
|
64
|
+
attr
|
|
65
|
+
for ty in sorted(reversed(type(obj).__mro__), key=lambda _ty: _ty.__dict__.get('__repr_priority__', 0))
|
|
66
|
+
for attr in ty.__dict__.get('__repr_attrs__', [])]
|
|
67
|
+
else:
|
|
68
|
+
attrs = obj.__repr_attrs__
|
|
69
|
+
return '%s@%s(%s)' % (
|
|
70
|
+
type(obj).__name__,
|
|
71
|
+
hex(id(obj))[2:],
|
|
72
|
+
', '.join('%s=%s' % (attr, '<self>' if value is obj else _repr(value))
|
|
73
|
+
for attr in attrs for value in [getattr(obj, attr)]))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@_repr_guard
|
|
77
|
+
def build_repr(obj, *attrs):
|
|
78
|
+
return '%s@%s(%s)' % (
|
|
79
|
+
type(obj).__name__,
|
|
80
|
+
hex(id(obj))[2:],
|
|
81
|
+
', '.join('%s=%r' % (attr, getattr(obj, attr)) for attr in attrs))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@_basic
|
|
85
|
+
@lang.cls_dct_fn()
|
|
86
|
+
def repr(cls_dct, *attrs, mro=False, priority=None):
|
|
87
|
+
def __repr__(self):
|
|
88
|
+
return build_attr_repr(self, mro=mro)
|
|
89
|
+
|
|
90
|
+
cls_dct['__repr_attrs__'] = attrs
|
|
91
|
+
if priority is not None:
|
|
92
|
+
cls_dct['__repr_priority__'] = priority
|
|
93
|
+
cls_dct['__repr__'] = __repr__
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@lang.cls_dct_fn()
|
|
97
|
+
def bare_repr(cls_dct, *attrs):
|
|
98
|
+
def __repr__(self):
|
|
99
|
+
return lang.attr_repr(self, *attrs)
|
|
100
|
+
|
|
101
|
+
cls_dct['__repr__'] = __repr__
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@lang.cls_dct_fn()
|
|
105
|
+
def name_repr(cls_dct):
|
|
106
|
+
def __repr__(self):
|
|
107
|
+
return self.__name__
|
|
108
|
+
|
|
109
|
+
cls_dct['__repr__'] = __repr__
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@lang.cls_dct_fn()
|
|
113
|
+
def ne(cls_dct):
|
|
114
|
+
def __ne__(self, other):
|
|
115
|
+
return not (self == other)
|
|
116
|
+
|
|
117
|
+
cls_dct['__ne__'] = __ne__
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@lang.cls_dct_fn()
|
|
121
|
+
def no_order(cls_dct, *, raise_=None):
|
|
122
|
+
def fn(self, other):
|
|
123
|
+
if raise_ is None:
|
|
124
|
+
return NotImplemented
|
|
125
|
+
else:
|
|
126
|
+
raise raise_
|
|
127
|
+
|
|
128
|
+
for att in [
|
|
129
|
+
'__lt__',
|
|
130
|
+
'__le__',
|
|
131
|
+
'__gt__',
|
|
132
|
+
'__ge__',
|
|
133
|
+
]:
|
|
134
|
+
cls_dct[att] = fn
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@_basic
|
|
138
|
+
@lang.cls_dct_fn()
|
|
139
|
+
def hash_eq(cls_dct, *attrs):
|
|
140
|
+
def __hash__(self):
|
|
141
|
+
return hash(tuple(getattr(self, attr) for attr in attrs))
|
|
142
|
+
|
|
143
|
+
cls_dct['__hash__'] = __hash__
|
|
144
|
+
|
|
145
|
+
def __eq__(self, other):
|
|
146
|
+
if type(other) is not type(self):
|
|
147
|
+
return False
|
|
148
|
+
for attr in attrs:
|
|
149
|
+
if getattr(self, attr) != getattr(other, attr):
|
|
150
|
+
return False
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
cls_dct['__eq__'] = __eq__
|
|
154
|
+
|
|
155
|
+
ne(cls_dct=cls_dct)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@lang.cls_dct_fn()
|
|
159
|
+
def getter(cls_dct, *attrs):
|
|
160
|
+
for attr in attrs:
|
|
161
|
+
cls_dct[attr] = property(operator.attrgetter('_' + attr))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@lang.cls_dct_fn()
|
|
165
|
+
def not_implemented(cls_dct, *names, **kwargs):
|
|
166
|
+
wrapper = kwargs.pop('wrapper', lambda _: _)
|
|
167
|
+
if kwargs:
|
|
168
|
+
raise TypeError(kwargs)
|
|
169
|
+
ret = []
|
|
170
|
+
for name in names:
|
|
171
|
+
@wrapper
|
|
172
|
+
def not_implemented(self, *args, **kwargs):
|
|
173
|
+
raise NotImplementedError
|
|
174
|
+
|
|
175
|
+
not_implemented.__name__ = name
|
|
176
|
+
cls_dct[name] = not_implemented
|
|
177
|
+
ret.append(not_implemented)
|
|
178
|
+
return tuple(ret)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@lang.cls_dct_fn()
|
|
182
|
+
def abstract_method(cls_dct, *names):
|
|
183
|
+
return not_implemented(cls_dct, *names, wrapper=abc.abstractmethod)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@lang.cls_dct_fn()
|
|
187
|
+
def abstract_property(cls_dct, *names):
|
|
188
|
+
return not_implemented(cls_dct, *names, wrapper=abc.abstractmethod)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@lang.cls_dct_fn()
|
|
192
|
+
def abstract_hash_eq(cls_dct):
|
|
193
|
+
return not_implemented(cls_dct, '__hash__', '__eq__', '__ne__', wrapper=abc.abstractmethod)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import typing as ta
|
|
3
|
+
import weakref
|
|
4
|
+
|
|
5
|
+
from .. import c3
|
|
6
|
+
from .. import check
|
|
7
|
+
from .. import reflect as rfl
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
T = ta.TypeVar('T')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_IMPL_FUNC_CLS_SET_CACHE: ta.MutableMapping[ta.Callable, ta.FrozenSet[type]] = weakref.WeakKeyDictionary()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_impl_func_cls_set(func: ta.Callable) -> ta.FrozenSet[type]:
|
|
20
|
+
try:
|
|
21
|
+
return _IMPL_FUNC_CLS_SET_CACHE[func]
|
|
22
|
+
except KeyError:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
ann = getattr(func, '__annotations__', {})
|
|
26
|
+
if not ann:
|
|
27
|
+
raise TypeError(f'Invalid impl func: {func!r}')
|
|
28
|
+
|
|
29
|
+
_, cls = next(iter(ta.get_type_hints(func).items()))
|
|
30
|
+
if rfl.is_union_type(cls):
|
|
31
|
+
ret = frozenset(check.isinstance(arg, type) for arg in ta.get_args(cls))
|
|
32
|
+
else:
|
|
33
|
+
ret = frozenset([check.isinstance(cls, type)])
|
|
34
|
+
|
|
35
|
+
_IMPL_FUNC_CLS_SET_CACHE[func] = ret
|
|
36
|
+
return ret
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def find_impl(cls: type, registry: ta.Mapping[type, T]) -> ta.Optional[T]:
|
|
40
|
+
mro = c3.compose_mro(cls, registry.keys())
|
|
41
|
+
|
|
42
|
+
match: ta.Optional[type] = None
|
|
43
|
+
for t in mro:
|
|
44
|
+
if match is not None:
|
|
45
|
+
# If *match* is an implicit ABC but there is another unrelated, equally matching implicit ABC, refuse the
|
|
46
|
+
# temptation to guess.
|
|
47
|
+
if (
|
|
48
|
+
t in registry
|
|
49
|
+
and t not in cls.__mro__
|
|
50
|
+
and match not in cls.__mro__
|
|
51
|
+
and not issubclass(match, t)
|
|
52
|
+
):
|
|
53
|
+
raise RuntimeError(f'Ambiguous dispatch: {match} or {t}')
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
if t in registry:
|
|
57
|
+
match = t
|
|
58
|
+
|
|
59
|
+
if match is None:
|
|
60
|
+
return None
|
|
61
|
+
return registry.get(match)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
##
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Dispatcher(ta.Generic[T]):
|
|
68
|
+
def __init__(self) -> None:
|
|
69
|
+
super().__init__()
|
|
70
|
+
|
|
71
|
+
impls_by_arg_cls: dict[type, T] = {}
|
|
72
|
+
self._impls_by_arg_cls = impls_by_arg_cls
|
|
73
|
+
|
|
74
|
+
dispatch_cache: dict[ta.Any, ta.Optional[T]] = {}
|
|
75
|
+
self._get_dispatch_cache = lambda: dispatch_cache
|
|
76
|
+
|
|
77
|
+
def cache_remove(k, self_ref=weakref.ref(self)):
|
|
78
|
+
if (ref_self := self_ref()) is not None:
|
|
79
|
+
cache = ref_self._get_dispatch_cache() # noqa
|
|
80
|
+
try:
|
|
81
|
+
del cache[k]
|
|
82
|
+
except KeyError:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
cache_token: ta.Any = None
|
|
86
|
+
self._get_cache_token = lambda: cache_token
|
|
87
|
+
|
|
88
|
+
weakref_ref_ = weakref.ref
|
|
89
|
+
|
|
90
|
+
def dispatch(cls: type) -> ta.Optional[T]:
|
|
91
|
+
nonlocal cache_token
|
|
92
|
+
|
|
93
|
+
if cache_token is not None and (current_token := abc.get_cache_token()) != cache_token:
|
|
94
|
+
dispatch_cache.clear()
|
|
95
|
+
cache_token = current_token
|
|
96
|
+
|
|
97
|
+
cls_ref = weakref_ref_(cls)
|
|
98
|
+
try:
|
|
99
|
+
return dispatch_cache[cls_ref]
|
|
100
|
+
except KeyError:
|
|
101
|
+
pass
|
|
102
|
+
del cls_ref
|
|
103
|
+
|
|
104
|
+
impl: T | None
|
|
105
|
+
try:
|
|
106
|
+
impl = impls_by_arg_cls[cls]
|
|
107
|
+
except KeyError:
|
|
108
|
+
impl = find_impl(cls, impls_by_arg_cls)
|
|
109
|
+
|
|
110
|
+
dispatch_cache[weakref_ref_(cls, cache_remove)] = impl
|
|
111
|
+
return impl
|
|
112
|
+
|
|
113
|
+
self.dispatch = dispatch
|
|
114
|
+
|
|
115
|
+
def register(impl: T, cls_col: ta.Iterable[type]) -> T:
|
|
116
|
+
nonlocal cache_token
|
|
117
|
+
|
|
118
|
+
for cls in cls_col:
|
|
119
|
+
impls_by_arg_cls[cls] = impl
|
|
120
|
+
|
|
121
|
+
if cache_token is None and hasattr(cls, '__abstractmethods__'):
|
|
122
|
+
cache_token = abc.get_cache_token()
|
|
123
|
+
|
|
124
|
+
dispatch_cache.clear()
|
|
125
|
+
return impl
|
|
126
|
+
|
|
127
|
+
self.register = register
|
|
128
|
+
|
|
129
|
+
_get_cache_token: ta.Callable[[], int]
|
|
130
|
+
_get_dispatch_cache: ta.Callable[[], ta.Any]
|
|
131
|
+
|
|
132
|
+
def cache_size(self) -> int:
|
|
133
|
+
return len(self._get_dispatch_cache())
|
|
134
|
+
|
|
135
|
+
dispatch: ta.Callable[[type], ta.Optional[T]]
|
|
136
|
+
|
|
137
|
+
register: ta.Callable[[T, ta.Iterable[type]], T]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import typing as ta
|
|
3
|
+
|
|
4
|
+
from .. import check
|
|
5
|
+
from .dispatch import Dispatcher
|
|
6
|
+
from .dispatch import get_impl_func_cls_set
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# USE_EXTENSION = True
|
|
10
|
+
USE_EXTENSION = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def function(func):
|
|
14
|
+
disp = Dispatcher() # type: ignore
|
|
15
|
+
disp.register(func, [object])
|
|
16
|
+
|
|
17
|
+
func_name = getattr(func, '__name__', 'singledispatch function')
|
|
18
|
+
|
|
19
|
+
def register(impl, cls=None):
|
|
20
|
+
check.callable(impl)
|
|
21
|
+
cls_col: ta.Iterable[type]
|
|
22
|
+
if cls is None:
|
|
23
|
+
cls_col = get_impl_func_cls_set(impl)
|
|
24
|
+
else:
|
|
25
|
+
cls_col = frozenset([cls])
|
|
26
|
+
disp.register(impl, cls_col)
|
|
27
|
+
return impl
|
|
28
|
+
|
|
29
|
+
if not USE_EXTENSION:
|
|
30
|
+
disp_dispatch = disp.dispatch
|
|
31
|
+
|
|
32
|
+
@functools.wraps(func)
|
|
33
|
+
def wrapper(*args, **kwargs):
|
|
34
|
+
if not args:
|
|
35
|
+
raise TypeError(f'{func_name} requires at least 1 positional argument')
|
|
36
|
+
if (impl := disp_dispatch(type(args[0]))) is not None:
|
|
37
|
+
return impl(*args, **kwargs)
|
|
38
|
+
raise RuntimeError(f'No dispatch: {type(args[0])}')
|
|
39
|
+
wrapper.register = register # type: ignore
|
|
40
|
+
wrapper.dispatch = disp.dispatch # type: ignore
|
|
41
|
+
|
|
42
|
+
else:
|
|
43
|
+
from x.c._dispatch import function_wrapper # noqa
|
|
44
|
+
wrapper = function_wrapper(
|
|
45
|
+
disp.dispatch,
|
|
46
|
+
**{k: getattr(func, k) for k in functools.WRAPPER_ASSIGNMENTS if hasattr(func, k)},
|
|
47
|
+
__wrapped__=func,
|
|
48
|
+
register=register,
|
|
49
|
+
dispatcher=disp,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return wrapper
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- .super(instance[_cls], owner)
|
|
4
|
+
- ALT: A.f(super(), ... ? :/
|
|
5
|
+
- classmethod/staticmethod
|
|
6
|
+
"""
|
|
7
|
+
import functools
|
|
8
|
+
import typing as ta
|
|
9
|
+
import weakref
|
|
10
|
+
|
|
11
|
+
from .. import check
|
|
12
|
+
from .dispatch import Dispatcher
|
|
13
|
+
from .dispatch import get_impl_func_cls_set
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
T = ta.TypeVar('T')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_mro_dct(instance_cls: type, owner_cls: ta.Optional[type] = None) -> ta.Mapping[str, ta.Any]:
|
|
20
|
+
if owner_cls is None:
|
|
21
|
+
owner_cls = instance_cls
|
|
22
|
+
mro = instance_cls.__mro__[-2::-1]
|
|
23
|
+
try:
|
|
24
|
+
pos = mro.index(owner_cls)
|
|
25
|
+
except ValueError:
|
|
26
|
+
raise TypeError(f'Owner class {owner_cls} not in mro of instance class {instance_cls}')
|
|
27
|
+
dct: dict[str, ta.Any] = {}
|
|
28
|
+
for cur_cls in mro[:pos + 1]:
|
|
29
|
+
dct.update(cur_cls.__dict__)
|
|
30
|
+
return dct
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Method:
|
|
34
|
+
def __init__(self, func: ta.Callable) -> None:
|
|
35
|
+
if not callable(func) and not hasattr(func, '__get__'): # type: ignore
|
|
36
|
+
raise TypeError(f'{func!r} is not callable or a descriptor')
|
|
37
|
+
|
|
38
|
+
self._func = func
|
|
39
|
+
|
|
40
|
+
self._impls: ta.MutableSet[ta.Callable] = weakref.WeakSet()
|
|
41
|
+
|
|
42
|
+
# bpo-45678: special-casing for classmethod/staticmethod in Python <=3.9, as functools.update_wrapper doesn't
|
|
43
|
+
# work properly in singledispatchmethod.__get__ if it is applied to an unbound classmethod/staticmethod
|
|
44
|
+
unwrapped_func: ta.Any
|
|
45
|
+
if isinstance(func, (staticmethod, classmethod)):
|
|
46
|
+
unwrapped_func = func.__func__
|
|
47
|
+
else:
|
|
48
|
+
unwrapped_func = func
|
|
49
|
+
self._unwrapped_func = unwrapped_func
|
|
50
|
+
self._is_abstractmethod = getattr(func, '__isabstractmethod__', False) # noqa
|
|
51
|
+
self.update_wrapper(self)
|
|
52
|
+
|
|
53
|
+
self._dispatch_func_cache: dict[ta.Any, ta.Callable] = {}
|
|
54
|
+
|
|
55
|
+
def dispatch_func_cache_remove(k, self_ref=weakref.ref(self)):
|
|
56
|
+
if (ref_self := self_ref()) is not None:
|
|
57
|
+
cache = ref_self._dispatch_func_cache # noqa
|
|
58
|
+
try:
|
|
59
|
+
del cache[k]
|
|
60
|
+
except KeyError:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
self._dispatch_func_cache_remove = dispatch_func_cache_remove
|
|
64
|
+
|
|
65
|
+
def __repr__(self) -> str:
|
|
66
|
+
return f'<{type(self).__module__}.{type(self).__qualname__}:{self._func} at 0x{id(self):x}>'
|
|
67
|
+
|
|
68
|
+
def update_wrapper(self, wrapper: T) -> T:
|
|
69
|
+
for attr in functools.WRAPPER_ASSIGNMENTS:
|
|
70
|
+
try:
|
|
71
|
+
value = getattr(self._unwrapped_func, attr)
|
|
72
|
+
except AttributeError:
|
|
73
|
+
continue
|
|
74
|
+
setattr(wrapper, attr, value)
|
|
75
|
+
setattr(wrapper, '__isabstractmethod__', self._is_abstractmethod) # noqa
|
|
76
|
+
return wrapper
|
|
77
|
+
|
|
78
|
+
def register(self, impl: T) -> T:
|
|
79
|
+
# bpo-39679: in Python <= 3.9, classmethods and staticmethods don't inherit __annotations__ of the wrapped
|
|
80
|
+
# function (fixed in 3.10+ as a side-effect of bpo-43682) but we need that for annotation-derived
|
|
81
|
+
# singledispatches. So we add that just-in-time here.
|
|
82
|
+
if isinstance(impl, (staticmethod, classmethod)):
|
|
83
|
+
impl.__annotations__ = getattr(impl.__func__, '__annotations__', {})
|
|
84
|
+
|
|
85
|
+
check.callable(impl)
|
|
86
|
+
if impl not in self._impls:
|
|
87
|
+
self._impls.add(impl) # type: ignore
|
|
88
|
+
self._dispatch_func_cache.clear()
|
|
89
|
+
|
|
90
|
+
return impl
|
|
91
|
+
|
|
92
|
+
def build_attr_dispatcher(self, instance_cls: type, owner_cls: ta.Optional[type] = None) -> Dispatcher[str]:
|
|
93
|
+
disp: Dispatcher[str] = Dispatcher()
|
|
94
|
+
|
|
95
|
+
mro_dct = build_mro_dct(instance_cls, owner_cls)
|
|
96
|
+
seen: ta.Mapping[ta.Any, str] = {}
|
|
97
|
+
for nam, att in mro_dct.items():
|
|
98
|
+
if att in self._impls:
|
|
99
|
+
try:
|
|
100
|
+
ex_nam = seen[att]
|
|
101
|
+
except KeyError:
|
|
102
|
+
pass
|
|
103
|
+
else:
|
|
104
|
+
raise TypeError(f'Duplicate impl: {owner_cls} {instance_cls} {nam} {ex_nam}')
|
|
105
|
+
disp.register(nam, get_impl_func_cls_set(att))
|
|
106
|
+
|
|
107
|
+
return disp
|
|
108
|
+
|
|
109
|
+
def build_dispatch_func(self, disp: Dispatcher[str]) -> ta.Callable:
|
|
110
|
+
dispatch = disp.dispatch
|
|
111
|
+
type_ = type
|
|
112
|
+
getattr_ = getattr
|
|
113
|
+
base_func = self._func
|
|
114
|
+
func_name = getattr(base_func, '__name__', 'singledispatch method')
|
|
115
|
+
|
|
116
|
+
def __call__(self, *args, **kwargs): # noqa
|
|
117
|
+
if not args:
|
|
118
|
+
raise TypeError(f'{func_name} requires at least 1 positional argument')
|
|
119
|
+
if (impl_att := dispatch(type_(args[0]))) is not None:
|
|
120
|
+
fn = getattr_(self, impl_att)
|
|
121
|
+
return fn(*args, **kwargs)
|
|
122
|
+
return base_func.__get__(self)(*args, **kwargs) # noqa
|
|
123
|
+
|
|
124
|
+
self.update_wrapper(__call__)
|
|
125
|
+
return __call__
|
|
126
|
+
|
|
127
|
+
def get_dispatch_func(self, instance_cls: type) -> ta.Callable:
|
|
128
|
+
cls_ref = weakref.ref(instance_cls)
|
|
129
|
+
try:
|
|
130
|
+
return self._dispatch_func_cache[cls_ref]
|
|
131
|
+
except KeyError:
|
|
132
|
+
pass
|
|
133
|
+
del cls_ref
|
|
134
|
+
|
|
135
|
+
att_disp = self.build_attr_dispatcher(instance_cls)
|
|
136
|
+
func = self.build_dispatch_func(att_disp)
|
|
137
|
+
self._dispatch_func_cache[weakref.ref(instance_cls, self._dispatch_func_cache_remove)] = func
|
|
138
|
+
return func
|
|
139
|
+
|
|
140
|
+
def __get__(self, instance, owner=None):
|
|
141
|
+
if instance is None:
|
|
142
|
+
# FIXME: classmethod/staticmethod
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
instance_cls = type(instance)
|
|
146
|
+
try:
|
|
147
|
+
func = self._dispatch_func_cache[weakref.ref(instance_cls)]
|
|
148
|
+
except KeyError:
|
|
149
|
+
func = self.get_dispatch_func(instance_cls)
|
|
150
|
+
return func.__get__(instance, owner) # noqa
|
|
151
|
+
|
|
152
|
+
def __call__(self, instance, *args, **kwargs):
|
|
153
|
+
instance_cls = type(instance)
|
|
154
|
+
try:
|
|
155
|
+
func = self._dispatch_func_cache[weakref.ref(instance_cls)]
|
|
156
|
+
except KeyError:
|
|
157
|
+
func = self.get_dispatch_func(instance_cls)
|
|
158
|
+
return func.__get__(instance)(*args, **kwargs) # noqa
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def method(func):
|
|
162
|
+
return Method(func)
|
omlish/docker.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- merged compose configs: https://github.com/wrmsr/bane/blob/27647abdcfb323b73e6982a5c318c7029496b203/core/dev/docker/compose.go#L38
|
|
4
|
+
- https://github.com/mag37/dockcheck/blob/3d122f2b868eb53a25a3014f0f6bd499390a3a29/dockcheck.sh
|
|
5
|
+
- https://github.com/regclient/regclient
|
|
6
|
+
- https://stackoverflow.com/questions/71409458/how-to-download-docker-image-using-http-api-using-docker-hub-credentials
|
|
7
|
+
- https://stackoverflow.com/questions/55386202/how-can-i-use-the-docker-registry-api-to-pull-information-about-a-container-get
|
|
8
|
+
- https://ops.tips/blog/inspecting-docker-image-without-pull/
|
|
9
|
+
|
|
10
|
+
repo=library/nginx
|
|
11
|
+
token=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull" | jq -r '.token')
|
|
12
|
+
curl -H "Authorization: Bearer $token" -s "https://registry-1.docker.io/v2/${repo}/tags/list" | jq
|
|
13
|
+
api="application/vnd.docker.distribution.manifest.v2+json"
|
|
14
|
+
apil="application/vnd.docker.distribution.manifest.list.v2+json"
|
|
15
|
+
curl -H "Accept: ${api}" -H "Accept: ${apil}" -H "Authorization: Bearer $token" -s "https://registry-1.docker.io/v2/${repo}/manifests/latest" | jq .
|
|
16
|
+
""" # noqa
|
|
17
|
+
import datetime
|
|
18
|
+
import re
|
|
19
|
+
import shlex
|
|
20
|
+
import subprocess
|
|
21
|
+
import typing as ta
|
|
22
|
+
|
|
23
|
+
from . import check
|
|
24
|
+
from . import dataclasses as dc
|
|
25
|
+
from . import json
|
|
26
|
+
from . import lang
|
|
27
|
+
from . import marshal as msh
|
|
28
|
+
|
|
29
|
+
if ta.TYPE_CHECKING:
|
|
30
|
+
import yaml
|
|
31
|
+
else:
|
|
32
|
+
yaml = lang.proxy_import('yaml')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dc.dataclass(frozen=True)
|
|
36
|
+
class PsItem(lang.Final):
|
|
37
|
+
dc.metadata(msh.ObjectMetadata(
|
|
38
|
+
field_naming=msh.Naming.CAMEL,
|
|
39
|
+
unknown_field='x',
|
|
40
|
+
))
|
|
41
|
+
|
|
42
|
+
command: str
|
|
43
|
+
created_at: datetime.datetime
|
|
44
|
+
id: str = dc.field(metadata={msh.FieldMetadata: msh.FieldMetadata(name='ID')})
|
|
45
|
+
image: str
|
|
46
|
+
labels: str
|
|
47
|
+
local_volumes: str
|
|
48
|
+
mounts: str
|
|
49
|
+
names: str
|
|
50
|
+
networks: str
|
|
51
|
+
ports: str
|
|
52
|
+
running_for: str
|
|
53
|
+
size: str
|
|
54
|
+
state: str
|
|
55
|
+
status: str
|
|
56
|
+
|
|
57
|
+
x: ta.Mapping[str, ta.Any] | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Port(ta.NamedTuple):
|
|
61
|
+
ip: str
|
|
62
|
+
from_port: int
|
|
63
|
+
to_port: int
|
|
64
|
+
proto: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_PORT_PAT = re.compile(r'(?P<ip>[^:]+):(?P<from_port>\d+)->(?P<to_port>\d+)/(?P<proto>\w+)')
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def parse_port(s: str) -> Port:
|
|
71
|
+
# '0.0.0.0:35221->22/tcp, 0.0.0.0:35220->8000/tcp'
|
|
72
|
+
m = check.not_none(_PORT_PAT.fullmatch(s))
|
|
73
|
+
return Port(
|
|
74
|
+
m.group('ip'),
|
|
75
|
+
int(m.group('from_port')),
|
|
76
|
+
int(m.group('to_port')),
|
|
77
|
+
m.group('proto'),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cli_ps() -> list[PsItem]:
|
|
82
|
+
o = subprocess.check_output([
|
|
83
|
+
'docker',
|
|
84
|
+
'ps',
|
|
85
|
+
'--no-trunc',
|
|
86
|
+
'--format', '{{json .}}',
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
ret: list[PsItem] = []
|
|
90
|
+
for l in o.decode().splitlines():
|
|
91
|
+
d = json.loads(l)
|
|
92
|
+
pi = msh.unmarshal(d, PsItem)
|
|
93
|
+
ret.append(pi)
|
|
94
|
+
|
|
95
|
+
return ret
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dc.dataclass(frozen=True)
|
|
99
|
+
class Inspect(lang.Final):
|
|
100
|
+
dc.metadata(msh.ObjectMetadata(
|
|
101
|
+
field_naming=msh.Naming.CAMEL,
|
|
102
|
+
unknown_field='x',
|
|
103
|
+
))
|
|
104
|
+
|
|
105
|
+
id: str
|
|
106
|
+
created: datetime.datetime
|
|
107
|
+
|
|
108
|
+
x: ta.Mapping[str, ta.Any] | None = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def cli_inspect(ids: list[str]) -> list[Inspect]:
|
|
112
|
+
o = subprocess.check_output(['docker', 'inspect', *ids])
|
|
113
|
+
return msh.unmarshal(json.loads(o.decode()), list[Inspect])
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ComposeConfig:
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
prefix: str,
|
|
120
|
+
*,
|
|
121
|
+
compose_path: str | None = None,
|
|
122
|
+
) -> None:
|
|
123
|
+
super().__init__()
|
|
124
|
+
|
|
125
|
+
self._prefix = prefix
|
|
126
|
+
self._compose_path = compose_path
|
|
127
|
+
|
|
128
|
+
@lang.cached_function
|
|
129
|
+
def get_config(self) -> ta.Mapping[str, ta.Any]:
|
|
130
|
+
with open(check.not_none(self._compose_path), 'r') as f:
|
|
131
|
+
buf = f.read()
|
|
132
|
+
dct = yaml.safe_load(buf)
|
|
133
|
+
|
|
134
|
+
ret = {}
|
|
135
|
+
for n, c in dct['services'].items():
|
|
136
|
+
check.state(n.startswith(self._prefix))
|
|
137
|
+
ret[n[len(self._prefix):]] = c
|
|
138
|
+
|
|
139
|
+
return ret
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def timebomb_payload(delay_s: int | float, name: str = 'omlish-timebomb') -> str:
|
|
143
|
+
return (
|
|
144
|
+
'('
|
|
145
|
+
f'echo {shlex.quote(name)} && '
|
|
146
|
+
f'sleep {delay_s:g} && '
|
|
147
|
+
'sh -c \'killall5 -9 -o $PPID -o $$ ; kill 1\''
|
|
148
|
+
') &'
|
|
149
|
+
)
|