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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- more cache-recursive reuse - fields, mro, etc
|
|
4
|
+
"""
|
|
5
|
+
import dataclasses as dc
|
|
6
|
+
import inspect
|
|
7
|
+
import sys
|
|
8
|
+
import typing as ta
|
|
9
|
+
import weakref
|
|
10
|
+
|
|
11
|
+
from ... import cached
|
|
12
|
+
from ... import check
|
|
13
|
+
from ... import collections as col
|
|
14
|
+
from ... import lang
|
|
15
|
+
from ... import reflect as rfl
|
|
16
|
+
from .fields import field_type
|
|
17
|
+
from .internals import FIELDS_ATTR
|
|
18
|
+
from .internals import FieldType
|
|
19
|
+
from .internals import Params
|
|
20
|
+
from .internals import is_kw_only
|
|
21
|
+
from .metadata import METADATA_ATTR
|
|
22
|
+
from .metadata import Metadata
|
|
23
|
+
from .metadata import get_merged_metadata
|
|
24
|
+
from .params import PARAMS_ATTR
|
|
25
|
+
from .params import Params12
|
|
26
|
+
from .params import ParamsExtras
|
|
27
|
+
from .params import get_params
|
|
28
|
+
from .params import get_params12
|
|
29
|
+
from .params import get_params_extras
|
|
30
|
+
from .utils import Namespace
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
MISSING = dc.MISSING
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ClassInfo:
|
|
37
|
+
|
|
38
|
+
def __init__(self, cls: type, *, _constructing: bool = False) -> None:
|
|
39
|
+
check.isinstance(cls, type)
|
|
40
|
+
self._constructing = _constructing
|
|
41
|
+
if not _constructing:
|
|
42
|
+
check.arg(dc.is_dataclass(cls))
|
|
43
|
+
super().__init__()
|
|
44
|
+
self._cls: type = cls
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def cls(self) -> type:
|
|
48
|
+
return self._cls # noqa
|
|
49
|
+
|
|
50
|
+
@cached.property
|
|
51
|
+
def globals(self) -> Namespace:
|
|
52
|
+
if self._cls.__module__ in sys.modules:
|
|
53
|
+
return sys.modules[self._cls.__module__].__dict__
|
|
54
|
+
else:
|
|
55
|
+
return {}
|
|
56
|
+
|
|
57
|
+
@cached.property
|
|
58
|
+
def cls_annotations(self) -> dict[str, ta.Any]:
|
|
59
|
+
return inspect.get_annotations(self._cls)
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
|
|
63
|
+
@cached.property
|
|
64
|
+
def params(self) -> Params:
|
|
65
|
+
return get_params(self._cls)
|
|
66
|
+
|
|
67
|
+
@cached.property
|
|
68
|
+
def cls_params(self) -> Params | None:
|
|
69
|
+
return self._cls.__dict__.get(PARAMS_ATTR)
|
|
70
|
+
|
|
71
|
+
@cached.property
|
|
72
|
+
def params12(self) -> Params12:
|
|
73
|
+
return get_params12(self._cls)
|
|
74
|
+
|
|
75
|
+
@cached.property
|
|
76
|
+
def params_extras(self) -> ParamsExtras:
|
|
77
|
+
return get_params_extras(self._cls)
|
|
78
|
+
|
|
79
|
+
@cached.property
|
|
80
|
+
def cls_params_extras(self) -> ParamsExtras | None:
|
|
81
|
+
return (self.cls_metadata or {}).get(ParamsExtras)
|
|
82
|
+
|
|
83
|
+
@cached.property
|
|
84
|
+
def cls_metadata(self) -> Metadata | None:
|
|
85
|
+
return self._cls.__dict__.get(METADATA_ATTR)
|
|
86
|
+
|
|
87
|
+
@cached.property
|
|
88
|
+
def merged_metadata(self) -> Metadata:
|
|
89
|
+
return get_merged_metadata(self._cls)
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
|
|
93
|
+
class _FoundFields(ta.NamedTuple):
|
|
94
|
+
fields: dict[str, dc.Field]
|
|
95
|
+
field_owners: dict[str, type]
|
|
96
|
+
|
|
97
|
+
@lang.cached_function
|
|
98
|
+
def _find_fields(self) -> _FoundFields:
|
|
99
|
+
if self._constructing:
|
|
100
|
+
check.in_(FIELDS_ATTR, self._cls.__dict__)
|
|
101
|
+
|
|
102
|
+
fields: dict[str, dc.Field] = {}
|
|
103
|
+
field_owners: dict[str, type] = {}
|
|
104
|
+
|
|
105
|
+
for b in self._cls.__mro__[-1:0:-1]:
|
|
106
|
+
base_fields = getattr(b, FIELDS_ATTR, None)
|
|
107
|
+
if base_fields is not None:
|
|
108
|
+
for name in reflect(b).cls_annotations:
|
|
109
|
+
try:
|
|
110
|
+
f = base_fields[name]
|
|
111
|
+
except KeyError:
|
|
112
|
+
continue
|
|
113
|
+
fields[f.name] = f
|
|
114
|
+
field_owners[f.name] = b
|
|
115
|
+
|
|
116
|
+
cls_fields = getattr(self._cls, FIELDS_ATTR)
|
|
117
|
+
for name, ann in self.cls_annotations.items():
|
|
118
|
+
if is_kw_only(self._cls, ann):
|
|
119
|
+
continue
|
|
120
|
+
fields[name] = cls_fields[name]
|
|
121
|
+
field_owners[name] = self._cls
|
|
122
|
+
|
|
123
|
+
return ClassInfo._FoundFields(fields, field_owners)
|
|
124
|
+
|
|
125
|
+
@cached.property
|
|
126
|
+
def fields(self) -> ta.Mapping[str, dc.Field]:
|
|
127
|
+
return self._find_fields().fields
|
|
128
|
+
|
|
129
|
+
@cached.property
|
|
130
|
+
def instance_fields(self) -> ta.Sequence[dc.Field]:
|
|
131
|
+
return [f for f in self.fields.values() if field_type(f) is FieldType.INSTANCE]
|
|
132
|
+
|
|
133
|
+
@cached.property
|
|
134
|
+
def field_owners(self) -> ta.Mapping[str, type]:
|
|
135
|
+
return self._find_fields().field_owners
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
|
|
139
|
+
@cached.property
|
|
140
|
+
def generic_mro(self) -> ta.Sequence[rfl.Type]:
|
|
141
|
+
return rfl.ALIAS_UPDATING_GENERIC_SUBSTITUTION.generic_mro(self._cls)
|
|
142
|
+
|
|
143
|
+
@cached.property
|
|
144
|
+
def generic_mro_lookup(self) -> ta.Mapping[type, rfl.Type]:
|
|
145
|
+
return col.unique_dict((check.not_none(rfl.get_concrete_type(g)), g) for g in self.generic_mro)
|
|
146
|
+
|
|
147
|
+
@cached.property
|
|
148
|
+
def generic_replaced_field_types(self) -> ta.Mapping[str, rfl.Type]:
|
|
149
|
+
ret: dict[str, ta.Any] = {}
|
|
150
|
+
for f in self.fields.values():
|
|
151
|
+
fo = self.field_owners[f.name]
|
|
152
|
+
go = self.generic_mro_lookup[fo]
|
|
153
|
+
tvr = rfl.get_type_var_replacements(go)
|
|
154
|
+
fty = rfl.type_(f.type)
|
|
155
|
+
rty = rfl.replace_type_vars(fty, tvr, update_aliases=True)
|
|
156
|
+
ret[f.name] = rty
|
|
157
|
+
return ret
|
|
158
|
+
|
|
159
|
+
@cached.property
|
|
160
|
+
def generic_replaced_field_annotations(self) -> ta.Mapping[str, ta.Any]:
|
|
161
|
+
return {k: rfl.to_annotation(v) for k, v in self.generic_replaced_field_types.items()}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
_CLASS_INFO_CACHE: ta.MutableMapping[type, ClassInfo] = weakref.WeakKeyDictionary()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def reflect(obj: ta.Any) -> ClassInfo:
|
|
168
|
+
cls = obj if isinstance(obj, type) else type(obj)
|
|
169
|
+
try:
|
|
170
|
+
return _CLASS_INFO_CACHE[cls]
|
|
171
|
+
except KeyError:
|
|
172
|
+
_CLASS_INFO_CACHE[cls] = info = ClassInfo(cls)
|
|
173
|
+
return info
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import dataclasses as dc
|
|
2
|
+
|
|
3
|
+
from .fields import field_type
|
|
4
|
+
from .internals import FIELDS_ATTR
|
|
5
|
+
from .internals import FieldType
|
|
6
|
+
from .internals import is_dataclass_instance
|
|
7
|
+
from .processing import Processor
|
|
8
|
+
from .utils import set_new_attribute
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
MISSING = dc.MISSING
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def replace(obj, /, **changes):
|
|
15
|
+
if not is_dataclass_instance(obj):
|
|
16
|
+
raise TypeError('replace() should be called on dataclass instances')
|
|
17
|
+
return _replace(obj, **changes)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _replace(obj, /, **changes):
|
|
21
|
+
for f in getattr(obj, FIELDS_ATTR).values():
|
|
22
|
+
if (ft := field_type(f)) is FieldType.CLASS:
|
|
23
|
+
continue
|
|
24
|
+
|
|
25
|
+
if not f.init:
|
|
26
|
+
if f.name in changes:
|
|
27
|
+
raise TypeError(f'field {f.name} is declared with init=False, it cannot be specified with replace()')
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
if f.name not in changes:
|
|
31
|
+
if ft is FieldType.INIT and f.default is MISSING:
|
|
32
|
+
raise TypeError(f'InitVar {f.name!r} must be specified with replace()')
|
|
33
|
+
changes[f.name] = getattr(obj, f.name)
|
|
34
|
+
|
|
35
|
+
return obj.__class__(**changes)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ReplaceProcessor(Processor):
|
|
39
|
+
def _process(self) -> None:
|
|
40
|
+
set_new_attribute(self._cls, '__replace__', _replace)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import dataclasses as dc
|
|
2
|
+
import reprlib
|
|
3
|
+
import typing as ta
|
|
4
|
+
|
|
5
|
+
from .processing import Processor
|
|
6
|
+
from .utils import Namespace
|
|
7
|
+
from .utils import create_fn
|
|
8
|
+
from .utils import set_new_attribute
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def repr_fn(
|
|
12
|
+
fields: ta.Sequence[dc.Field],
|
|
13
|
+
globals: Namespace,
|
|
14
|
+
) -> ta.Callable:
|
|
15
|
+
fn = create_fn(
|
|
16
|
+
'__repr__',
|
|
17
|
+
('self',),
|
|
18
|
+
[
|
|
19
|
+
'return f"{self.__class__.__qualname__}(' +
|
|
20
|
+
', '.join([f"{f.name}={{self.{f.name}!r}}" for f in fields]) +
|
|
21
|
+
')"'
|
|
22
|
+
],
|
|
23
|
+
globals=globals,
|
|
24
|
+
)
|
|
25
|
+
return reprlib.recursive_repr()(fn)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ReprProcessor(Processor):
|
|
29
|
+
def _process(self) -> None:
|
|
30
|
+
if not self._info.params.repr:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
flds = [f for f in self._info.instance_fields if f.repr]
|
|
34
|
+
set_new_attribute(self._cls, '__repr__', repr_fn(flds, self._info.globals)) # noqa
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
from ... import lang
|
|
4
|
+
from .fields import field_assign
|
|
5
|
+
from .init import get_init_fields
|
|
6
|
+
from .params import get_field_extras
|
|
7
|
+
from .processing import Processor
|
|
8
|
+
from .utils import create_fn
|
|
9
|
+
from .utils import set_new_attribute
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OverridesProcessor(Processor):
|
|
13
|
+
def _process(self) -> None:
|
|
14
|
+
for f in self._info.instance_fields:
|
|
15
|
+
fx = get_field_extras(f)
|
|
16
|
+
if not fx.override:
|
|
17
|
+
continue
|
|
18
|
+
|
|
19
|
+
if self._info.params12.slots:
|
|
20
|
+
raise TypeError
|
|
21
|
+
|
|
22
|
+
self_name = '__dataclass_self__' if 'self' in self._info.fields else 'self'
|
|
23
|
+
|
|
24
|
+
getter = create_fn(
|
|
25
|
+
f.name,
|
|
26
|
+
(self_name,),
|
|
27
|
+
[f'return {self_name}.__dict__[{f.name!r}]'],
|
|
28
|
+
globals=self._info.globals,
|
|
29
|
+
return_type=lang.just(f.type),
|
|
30
|
+
)
|
|
31
|
+
prop = property(getter)
|
|
32
|
+
|
|
33
|
+
if not self._info.params.frozen:
|
|
34
|
+
setter = create_fn(
|
|
35
|
+
f.name,
|
|
36
|
+
(self_name, f'{f.name}: __dataclass_type_{f.name}__'),
|
|
37
|
+
[field_assign(self._info.params.frozen, f.name, f.name, self_name, fx.override)],
|
|
38
|
+
globals=self._info.globals,
|
|
39
|
+
locals={f'__dataclass_type_{f.name}__': f.type},
|
|
40
|
+
return_type=lang.just(None),
|
|
41
|
+
)
|
|
42
|
+
prop = prop.setter(setter)
|
|
43
|
+
|
|
44
|
+
set_new_attribute(
|
|
45
|
+
self._cls,
|
|
46
|
+
f.name,
|
|
47
|
+
prop,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class EqProcessor(Processor):
|
|
52
|
+
def _process(self) -> None:
|
|
53
|
+
if not self._info.params.eq:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
# flds = [f for f in self._info.instance_fields if f.compare]
|
|
57
|
+
# self_tuple = tuple_str('self', flds)
|
|
58
|
+
# other_tuple = tuple_str('other', flds)
|
|
59
|
+
# set_new_attribute(cls, '__eq__', _cmp_fn('__eq__', '==', self_tuple, other_tuple, globals=globals))
|
|
60
|
+
cmp_fields = (field for field in self._info.instance_fields if field.compare)
|
|
61
|
+
terms = [f'self.{field.name} == other.{field.name}' for field in cmp_fields]
|
|
62
|
+
field_comparisons = ' and '.join(terms) or 'True'
|
|
63
|
+
body = [
|
|
64
|
+
f'if self is other:',
|
|
65
|
+
f' return True',
|
|
66
|
+
f'if other.__class__ is self.__class__:',
|
|
67
|
+
f' return {field_comparisons}',
|
|
68
|
+
f'return NotImplemented',
|
|
69
|
+
]
|
|
70
|
+
func = create_fn('__eq__', ('self', 'other'), body, globals=self._info.globals)
|
|
71
|
+
set_new_attribute(self._cls, '__eq__', func)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class DocProcessor(Processor):
|
|
75
|
+
def _process(self) -> None:
|
|
76
|
+
if getattr(self._cls, '__doc__'):
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
text_sig = str(inspect.signature(self._cls)).replace(' -> None', '')
|
|
81
|
+
except (TypeError, ValueError):
|
|
82
|
+
text_sig = ''
|
|
83
|
+
self._cls.__doc__ = (self._cls.__name__ + text_sig)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class MatchArgsProcessor(Processor):
|
|
87
|
+
def _process(self) -> None:
|
|
88
|
+
if not self._info.params12.match_args:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
ifs = get_init_fields(self._info.fields.values())
|
|
92
|
+
set_new_attribute(self._cls, '__match_args__', tuple(f.name for f in ifs.std))
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import dataclasses as dc
|
|
2
|
+
import itertools
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
MISSING = dc.MISSING
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _dataclass_getstate(self):
|
|
9
|
+
return [getattr(self, f.name) for f in dc.fields(self)]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _dataclass_setstate(self, state):
|
|
13
|
+
for field, value in zip(dc.fields(self), state):
|
|
14
|
+
object.__setattr__(self, field.name, value)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_slots(cls):
|
|
18
|
+
match cls.__dict__.get('__slots__'):
|
|
19
|
+
# A class which does not define __slots__ at all is equivalent to a class defining
|
|
20
|
+
# __slots__ = ('__dict__', '__weakref__')
|
|
21
|
+
# `__dictoffset__` and `__weakrefoffset__` can tell us whether the base type has dict/weakref slots, in a way
|
|
22
|
+
# that works correctly for both Python classes and C extension types. Extension types don't use `__slots__` for
|
|
23
|
+
# slot creation
|
|
24
|
+
case None:
|
|
25
|
+
slots = []
|
|
26
|
+
if getattr(cls, '__weakrefoffset__', -1) != 0:
|
|
27
|
+
slots.append('__weakref__')
|
|
28
|
+
if getattr(cls, '__dictrefoffset__', -1) != 0:
|
|
29
|
+
slots.append('__dict__')
|
|
30
|
+
yield from slots
|
|
31
|
+
case str(slot):
|
|
32
|
+
yield slot
|
|
33
|
+
# Slots may be any iterable, but we cannot handle an iterator because it will already be (partially) consumed.
|
|
34
|
+
case iterable if not hasattr(iterable, '__next__'):
|
|
35
|
+
yield from iterable
|
|
36
|
+
case _:
|
|
37
|
+
raise TypeError(f"Slots of '{cls.__name__}' cannot be determined")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def add_slots(
|
|
41
|
+
cls: type,
|
|
42
|
+
is_frozen: bool,
|
|
43
|
+
weakref_slot: bool,
|
|
44
|
+
) -> type:
|
|
45
|
+
if '__slots__' in cls.__dict__:
|
|
46
|
+
raise TypeError(f'{cls.__name__} already specifies __slots__')
|
|
47
|
+
|
|
48
|
+
cls_dict = dict(cls.__dict__)
|
|
49
|
+
field_names = tuple(f.name for f in dc.fields(cls)) # noqa
|
|
50
|
+
|
|
51
|
+
inherited_slots = set(itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1])))
|
|
52
|
+
|
|
53
|
+
cls_dict['__slots__'] = tuple(
|
|
54
|
+
itertools.filterfalse(
|
|
55
|
+
inherited_slots.__contains__,
|
|
56
|
+
itertools.chain(
|
|
57
|
+
field_names,
|
|
58
|
+
('__weakref__',) if weakref_slot else ()
|
|
59
|
+
)
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
for field_name in field_names:
|
|
64
|
+
cls_dict.pop(field_name, None)
|
|
65
|
+
|
|
66
|
+
cls_dict.pop('__dict__', None)
|
|
67
|
+
cls_dict.pop('__weakref__', None)
|
|
68
|
+
|
|
69
|
+
qualname = getattr(cls, '__qualname__', None)
|
|
70
|
+
cls = type(cls)(cls.__name__, cls.__bases__, cls_dict)
|
|
71
|
+
if qualname is not None:
|
|
72
|
+
cls.__qualname__ = qualname
|
|
73
|
+
|
|
74
|
+
if is_frozen:
|
|
75
|
+
if '__getstate__' not in cls_dict:
|
|
76
|
+
cls.__getstate__ = _dataclass_getstate # type: ignore
|
|
77
|
+
if '__setstate__' not in cls_dict:
|
|
78
|
+
cls.__setstate__ = _dataclass_setstate # type: ignore
|
|
79
|
+
|
|
80
|
+
return cls
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import dataclasses as dc
|
|
2
|
+
import textwrap
|
|
3
|
+
import types
|
|
4
|
+
import typing as ta
|
|
5
|
+
|
|
6
|
+
from ... import check
|
|
7
|
+
from ... import lang
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
T = ta.TypeVar('T')
|
|
11
|
+
Namespace: ta.TypeAlias = ta.MutableMapping[str, ta.Any]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_fn(
|
|
15
|
+
name: str,
|
|
16
|
+
args: ta.Sequence[str],
|
|
17
|
+
body: ta.Sequence[str],
|
|
18
|
+
*,
|
|
19
|
+
globals: ta.Optional[Namespace] = None,
|
|
20
|
+
locals: ta.Optional[Namespace] = None,
|
|
21
|
+
return_type: lang.Maybe[ta.Any] = lang.empty(),
|
|
22
|
+
) -> ta.Callable:
|
|
23
|
+
check.not_isinstance(args, str)
|
|
24
|
+
check.not_isinstance(body, str)
|
|
25
|
+
|
|
26
|
+
if locals is None:
|
|
27
|
+
locals = {}
|
|
28
|
+
return_annotation = ''
|
|
29
|
+
if return_type.present:
|
|
30
|
+
locals['__dataclass_return_type__'] = return_type()
|
|
31
|
+
return_annotation = '->__dataclass_return_type__'
|
|
32
|
+
args = ','.join(args)
|
|
33
|
+
body = '\n'.join(f' {b}' for b in body)
|
|
34
|
+
|
|
35
|
+
txt = f' def {name}({args}){return_annotation}:\n{body}'
|
|
36
|
+
|
|
37
|
+
local_vars = ', '.join(locals.keys())
|
|
38
|
+
txt = f'def __create_fn__({local_vars}):\n{txt}\n return {name}'
|
|
39
|
+
ns: dict[str, ta.Any] = {}
|
|
40
|
+
exec(txt, globals, ns) # type: ignore
|
|
41
|
+
return ns['__create_fn__'](**locals)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# TODO: https://github.com/python/cpython/commit/8945b7ff55b87d11c747af2dad0e3e4d631e62d6
|
|
45
|
+
class FuncBuilder:
|
|
46
|
+
def __init__(self, globals: Namespace) -> None:
|
|
47
|
+
super().__init__()
|
|
48
|
+
|
|
49
|
+
self.names: list[str] = []
|
|
50
|
+
self.src: list[str] = []
|
|
51
|
+
self.globals = globals
|
|
52
|
+
self.locals: Namespace = {}
|
|
53
|
+
self.overwrite_errors: dict[str, bool] = {}
|
|
54
|
+
self.unconditional_adds: dict[str, bool] = {}
|
|
55
|
+
|
|
56
|
+
def add_fn(
|
|
57
|
+
self,
|
|
58
|
+
name: str,
|
|
59
|
+
args: ta.Sequence[str],
|
|
60
|
+
body: ta.Sequence[str],
|
|
61
|
+
*,
|
|
62
|
+
locals: ta.Optional[Namespace] = None,
|
|
63
|
+
return_type: lang.Maybe[ta.Any] = lang.empty(),
|
|
64
|
+
overwrite_error: bool = False,
|
|
65
|
+
unconditional_add: bool = False,
|
|
66
|
+
decorator: ta.Optional[str] = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
if locals is not None:
|
|
69
|
+
self.locals.update(locals)
|
|
70
|
+
|
|
71
|
+
# Keep track if this method is allowed to be overwritten if it already exists in the class. The error is
|
|
72
|
+
# method-specific, so keep it with the name. We'll use this when we generate all of the functions in the
|
|
73
|
+
# add_fns_to_class call. overwrite_error is either True, in which case we'll raise an error, or it's a string,
|
|
74
|
+
# in which case we'll raise an error and append this string.
|
|
75
|
+
if overwrite_error:
|
|
76
|
+
self.overwrite_errors[name] = overwrite_error
|
|
77
|
+
|
|
78
|
+
# Should this function always overwrite anything that's already in the class? The default is to not overwrite a
|
|
79
|
+
# function that already exists.
|
|
80
|
+
if unconditional_add:
|
|
81
|
+
self.unconditional_adds[name] = True
|
|
82
|
+
|
|
83
|
+
self.names.append(name)
|
|
84
|
+
|
|
85
|
+
if return_type.present:
|
|
86
|
+
self.locals[f'__dataclass_{name}_return_type__'] = return_type()
|
|
87
|
+
return_annotation = f' -> __dataclass_{name}_return_type__'
|
|
88
|
+
else:
|
|
89
|
+
return_annotation = ''
|
|
90
|
+
args = ', '.join(args)
|
|
91
|
+
body = textwrap.indent('\n'.join(body), ' ')
|
|
92
|
+
|
|
93
|
+
# Compute the text of the entire function, add it to the text we're generating.
|
|
94
|
+
deco_str = " {decorator}\n" if decorator else ""
|
|
95
|
+
self.src.append(f'{deco_str} def {name}({args}){return_annotation}:\n{body}')
|
|
96
|
+
|
|
97
|
+
def add_fns_to_class(self, cls: type) -> None:
|
|
98
|
+
# The source to all of the functions we're generating.
|
|
99
|
+
fns_src = '\n'.join(self.src)
|
|
100
|
+
|
|
101
|
+
# The locals they use.
|
|
102
|
+
local_vars = ','.join(self.locals)
|
|
103
|
+
|
|
104
|
+
# The names of all of the functions, used for the return value of the outer function. Need to handle the
|
|
105
|
+
# 0-tuple specially.
|
|
106
|
+
if not self.names:
|
|
107
|
+
return_names = '()'
|
|
108
|
+
else:
|
|
109
|
+
return_names = f'({",".join(self.names)},)'
|
|
110
|
+
|
|
111
|
+
# txt is the entire function we're going to execute, including the bodies of the functions we're defining.
|
|
112
|
+
# Here's a greatly simplified version:
|
|
113
|
+
# def __create_fn__():
|
|
114
|
+
# def __init__(self, x, y):
|
|
115
|
+
# self.x = x
|
|
116
|
+
# self.y = y
|
|
117
|
+
# @recursive_repr
|
|
118
|
+
# def __repr__(self):
|
|
119
|
+
# return f'cls(x={self.x!r},y={self.y!r})'
|
|
120
|
+
# return __init__,__repr__
|
|
121
|
+
|
|
122
|
+
txt = f'def __create_fn__({local_vars}):\n{fns_src}\n return {return_names}'
|
|
123
|
+
ns: dict[str, ta.Any] = {}
|
|
124
|
+
exec(txt, self.globals, ns) # type: ignore
|
|
125
|
+
fns = ns['__create_fn__'](**self.locals)
|
|
126
|
+
|
|
127
|
+
# Now that we've generated the functions, assign them into cls.
|
|
128
|
+
for name, fn in zip(self.names, fns):
|
|
129
|
+
fn.__qualname__ = f'{cls.__qualname__}.{fn.__name__}'
|
|
130
|
+
if self.unconditional_adds.get(name, False):
|
|
131
|
+
setattr(cls, name, fn)
|
|
132
|
+
else:
|
|
133
|
+
already_exists = set_new_attribute(cls, name, fn)
|
|
134
|
+
|
|
135
|
+
# See if it's an error to overwrite this particular function.
|
|
136
|
+
if already_exists and (msg_extra := self.overwrite_errors.get(name)):
|
|
137
|
+
error_msg = (f'Cannot overwrite attribute {fn.__name__} in class {cls.__name__}')
|
|
138
|
+
if msg_extra:
|
|
139
|
+
error_msg = f'{error_msg} {msg_extra}'
|
|
140
|
+
|
|
141
|
+
raise TypeError(error_msg)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def set_qualname(cls: type, value: T) -> T:
|
|
145
|
+
if isinstance(value, types.FunctionType):
|
|
146
|
+
value.__qualname__ = f'{cls.__qualname__}.{value.__name__}'
|
|
147
|
+
return value
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def set_new_attribute(cls: type, name: str, value: ta.Any) -> bool:
|
|
151
|
+
if name in cls.__dict__:
|
|
152
|
+
return True
|
|
153
|
+
set_qualname(cls, value)
|
|
154
|
+
setattr(cls, name, value)
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def tuple_str(obj_name: str, fields: ta.Iterable[dc.Field]) -> str:
|
|
159
|
+
# Return a string representing each field of obj_name as a tuple member. So, if fields is ['x', 'y'] and obj_name
|
|
160
|
+
# is "self", return "(self.x,self.y)".
|
|
161
|
+
|
|
162
|
+
# Special case for the 0-tuple.
|
|
163
|
+
if not fields:
|
|
164
|
+
return '()'
|
|
165
|
+
|
|
166
|
+
# Note the trailing comma, needed if this turns out to be a 1-tuple.
|
|
167
|
+
return f'({",".join([f"{obj_name}.{f.name}" for f in fields])},)'
|