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.

Files changed (187) hide show
  1. omlish/__about__.py +7 -0
  2. omlish/__init__.py +0 -0
  3. omlish/argparse.py +223 -0
  4. omlish/asyncs/__init__.py +17 -0
  5. omlish/asyncs/anyio.py +23 -0
  6. omlish/asyncs/asyncio.py +19 -0
  7. omlish/asyncs/asyncs.py +76 -0
  8. omlish/asyncs/futures.py +179 -0
  9. omlish/asyncs/trio.py +11 -0
  10. omlish/c3.py +173 -0
  11. omlish/cached.py +9 -0
  12. omlish/check.py +231 -0
  13. omlish/collections/__init__.py +63 -0
  14. omlish/collections/_abc.py +156 -0
  15. omlish/collections/_io_abc.py +78 -0
  16. omlish/collections/cache/__init__.py +11 -0
  17. omlish/collections/cache/descriptor.py +188 -0
  18. omlish/collections/cache/impl.py +485 -0
  19. omlish/collections/cache/types.py +37 -0
  20. omlish/collections/coerce.py +337 -0
  21. omlish/collections/frozen.py +148 -0
  22. omlish/collections/identity.py +106 -0
  23. omlish/collections/indexed.py +75 -0
  24. omlish/collections/mappings.py +127 -0
  25. omlish/collections/ordered.py +81 -0
  26. omlish/collections/persistent.py +36 -0
  27. omlish/collections/skiplist.py +193 -0
  28. omlish/collections/sorted.py +126 -0
  29. omlish/collections/treap.py +228 -0
  30. omlish/collections/treapmap.py +144 -0
  31. omlish/collections/unmodifiable.py +174 -0
  32. omlish/collections/utils.py +110 -0
  33. omlish/configs/__init__.py +0 -0
  34. omlish/configs/flattening.py +147 -0
  35. omlish/configs/props.py +64 -0
  36. omlish/dataclasses/__init__.py +83 -0
  37. omlish/dataclasses/impl/__init__.py +6 -0
  38. omlish/dataclasses/impl/api.py +260 -0
  39. omlish/dataclasses/impl/as_.py +76 -0
  40. omlish/dataclasses/impl/exceptions.py +2 -0
  41. omlish/dataclasses/impl/fields.py +148 -0
  42. omlish/dataclasses/impl/frozen.py +55 -0
  43. omlish/dataclasses/impl/hashing.py +85 -0
  44. omlish/dataclasses/impl/init.py +173 -0
  45. omlish/dataclasses/impl/internals.py +118 -0
  46. omlish/dataclasses/impl/main.py +150 -0
  47. omlish/dataclasses/impl/metaclass.py +126 -0
  48. omlish/dataclasses/impl/metadata.py +74 -0
  49. omlish/dataclasses/impl/order.py +47 -0
  50. omlish/dataclasses/impl/params.py +150 -0
  51. omlish/dataclasses/impl/processing.py +16 -0
  52. omlish/dataclasses/impl/reflect.py +173 -0
  53. omlish/dataclasses/impl/replace.py +40 -0
  54. omlish/dataclasses/impl/repr.py +34 -0
  55. omlish/dataclasses/impl/simple.py +92 -0
  56. omlish/dataclasses/impl/slots.py +80 -0
  57. omlish/dataclasses/impl/utils.py +167 -0
  58. omlish/defs.py +193 -0
  59. omlish/dispatch/__init__.py +3 -0
  60. omlish/dispatch/dispatch.py +137 -0
  61. omlish/dispatch/functions.py +52 -0
  62. omlish/dispatch/methods.py +162 -0
  63. omlish/docker.py +149 -0
  64. omlish/dynamic.py +220 -0
  65. omlish/graphs/__init__.py +0 -0
  66. omlish/graphs/dot/__init__.py +19 -0
  67. omlish/graphs/dot/items.py +162 -0
  68. omlish/graphs/dot/rendering.py +147 -0
  69. omlish/graphs/dot/utils.py +30 -0
  70. omlish/graphs/trees.py +249 -0
  71. omlish/http/__init__.py +0 -0
  72. omlish/http/consts.py +20 -0
  73. omlish/http/wsgi.py +34 -0
  74. omlish/inject/__init__.py +85 -0
  75. omlish/inject/binder.py +12 -0
  76. omlish/inject/bindings.py +49 -0
  77. omlish/inject/eagers.py +21 -0
  78. omlish/inject/elements.py +43 -0
  79. omlish/inject/exceptions.py +49 -0
  80. omlish/inject/impl/__init__.py +0 -0
  81. omlish/inject/impl/bindings.py +19 -0
  82. omlish/inject/impl/elements.py +154 -0
  83. omlish/inject/impl/injector.py +182 -0
  84. omlish/inject/impl/inspect.py +98 -0
  85. omlish/inject/impl/private.py +109 -0
  86. omlish/inject/impl/providers.py +132 -0
  87. omlish/inject/impl/scopes.py +198 -0
  88. omlish/inject/injector.py +40 -0
  89. omlish/inject/inspect.py +14 -0
  90. omlish/inject/keys.py +43 -0
  91. omlish/inject/managed.py +24 -0
  92. omlish/inject/overrides.py +18 -0
  93. omlish/inject/private.py +29 -0
  94. omlish/inject/providers.py +111 -0
  95. omlish/inject/proxy.py +48 -0
  96. omlish/inject/scopes.py +84 -0
  97. omlish/inject/types.py +21 -0
  98. omlish/iterators.py +184 -0
  99. omlish/json.py +194 -0
  100. omlish/lang/__init__.py +112 -0
  101. omlish/lang/cached.py +267 -0
  102. omlish/lang/classes/__init__.py +24 -0
  103. omlish/lang/classes/abstract.py +74 -0
  104. omlish/lang/classes/restrict.py +137 -0
  105. omlish/lang/classes/simple.py +120 -0
  106. omlish/lang/classes/test/__init__.py +0 -0
  107. omlish/lang/classes/test/test_abstract.py +89 -0
  108. omlish/lang/classes/test/test_restrict.py +71 -0
  109. omlish/lang/classes/test/test_simple.py +58 -0
  110. omlish/lang/classes/test/test_virtual.py +72 -0
  111. omlish/lang/classes/virtual.py +130 -0
  112. omlish/lang/clsdct.py +67 -0
  113. omlish/lang/cmp.py +63 -0
  114. omlish/lang/contextmanagers.py +249 -0
  115. omlish/lang/datetimes.py +67 -0
  116. omlish/lang/descriptors.py +52 -0
  117. omlish/lang/functions.py +126 -0
  118. omlish/lang/imports.py +153 -0
  119. omlish/lang/iterables.py +54 -0
  120. omlish/lang/maybes.py +136 -0
  121. omlish/lang/objects.py +103 -0
  122. omlish/lang/resolving.py +50 -0
  123. omlish/lang/strings.py +128 -0
  124. omlish/lang/typing.py +92 -0
  125. omlish/libc.py +532 -0
  126. omlish/logs/__init__.py +9 -0
  127. omlish/logs/_abc.py +247 -0
  128. omlish/logs/configs.py +62 -0
  129. omlish/logs/filters.py +9 -0
  130. omlish/logs/formatters.py +67 -0
  131. omlish/logs/utils.py +20 -0
  132. omlish/marshal/__init__.py +52 -0
  133. omlish/marshal/any.py +25 -0
  134. omlish/marshal/base.py +201 -0
  135. omlish/marshal/base64.py +25 -0
  136. omlish/marshal/dataclasses.py +115 -0
  137. omlish/marshal/datetimes.py +90 -0
  138. omlish/marshal/enums.py +43 -0
  139. omlish/marshal/exceptions.py +7 -0
  140. omlish/marshal/factories.py +129 -0
  141. omlish/marshal/global_.py +33 -0
  142. omlish/marshal/iterables.py +57 -0
  143. omlish/marshal/mappings.py +66 -0
  144. omlish/marshal/naming.py +17 -0
  145. omlish/marshal/objects.py +106 -0
  146. omlish/marshal/optionals.py +49 -0
  147. omlish/marshal/polymorphism.py +147 -0
  148. omlish/marshal/primitives.py +43 -0
  149. omlish/marshal/registries.py +57 -0
  150. omlish/marshal/standard.py +80 -0
  151. omlish/marshal/utils.py +23 -0
  152. omlish/marshal/uuids.py +29 -0
  153. omlish/marshal/values.py +30 -0
  154. omlish/math.py +184 -0
  155. omlish/os.py +32 -0
  156. omlish/reflect.py +359 -0
  157. omlish/replserver/__init__.py +5 -0
  158. omlish/replserver/__main__.py +4 -0
  159. omlish/replserver/console.py +247 -0
  160. omlish/replserver/server.py +146 -0
  161. omlish/runmodule.py +28 -0
  162. omlish/stats.py +342 -0
  163. omlish/term.py +222 -0
  164. omlish/testing/__init__.py +7 -0
  165. omlish/testing/pydevd.py +225 -0
  166. omlish/testing/pytest/__init__.py +8 -0
  167. omlish/testing/pytest/helpers.py +35 -0
  168. omlish/testing/pytest/inject/__init__.py +1 -0
  169. omlish/testing/pytest/inject/harness.py +159 -0
  170. omlish/testing/pytest/plugins/__init__.py +20 -0
  171. omlish/testing/pytest/plugins/_registry.py +6 -0
  172. omlish/testing/pytest/plugins/logging.py +13 -0
  173. omlish/testing/pytest/plugins/pycharm.py +54 -0
  174. omlish/testing/pytest/plugins/repeat.py +19 -0
  175. omlish/testing/pytest/plugins/skips.py +32 -0
  176. omlish/testing/pytest/plugins/spacing.py +19 -0
  177. omlish/testing/pytest/plugins/switches.py +70 -0
  178. omlish/testing/testing.py +102 -0
  179. omlish/text/__init__.py +0 -0
  180. omlish/text/delimit.py +171 -0
  181. omlish/text/indent.py +50 -0
  182. omlish/text/parts.py +265 -0
  183. omlish-0.0.0.dev1.dist-info/LICENSE +21 -0
  184. omlish-0.0.0.dev1.dist-info/METADATA +17 -0
  185. omlish-0.0.0.dev1.dist-info/RECORD +187 -0
  186. omlish-0.0.0.dev1.dist-info/WHEEL +5 -0
  187. 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])},)'