ducktools-classbuilder 0.5.1__py3-none-any.whl → 0.6.0__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 ducktools-classbuilder might be problematic. Click here for more details.
- ducktools/classbuilder/__init__.py +486 -202
- ducktools/classbuilder/__init__.pyi +140 -55
- ducktools/classbuilder/annotations.py +173 -0
- ducktools/classbuilder/annotations.pyi +26 -0
- ducktools/classbuilder/prefab.py +88 -229
- ducktools/classbuilder/prefab.pyi +26 -22
- ducktools_classbuilder-0.6.0.dist-info/METADATA +318 -0
- ducktools_classbuilder-0.6.0.dist-info/RECORD +12 -0
- ducktools_classbuilder-0.5.1.dist-info/METADATA +0 -270
- ducktools_classbuilder-0.5.1.dist-info/RECORD +0 -10
- {ducktools_classbuilder-0.5.1.dist-info → ducktools_classbuilder-0.6.0.dist-info}/LICENSE.md +0 -0
- {ducktools_classbuilder-0.5.1.dist-info → ducktools_classbuilder-0.6.0.dist-info}/WHEEL +0 -0
- {ducktools_classbuilder-0.5.1.dist-info → ducktools_classbuilder-0.6.0.dist-info}/top_level.txt +0 -0
|
@@ -19,18 +19,39 @@
|
|
|
19
19
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
# In this module there are some internal bits of circular logic.
|
|
24
|
+
#
|
|
25
|
+
# 'Field' needs to exist in order to be used in gatherers, but is itself a
|
|
26
|
+
# partially constructed class. These constructed attributes are placed on
|
|
27
|
+
# 'Field' post construction.
|
|
28
|
+
#
|
|
29
|
+
# The 'SlotMakerMeta' metaclass generates 'Field' instances to go in __slots__
|
|
30
|
+
# but is also the metaclass used to construct 'Field'.
|
|
31
|
+
# Field itself sidesteps this by defining __slots__ to avoid that branch.
|
|
32
|
+
|
|
22
33
|
import sys
|
|
23
34
|
|
|
24
|
-
|
|
35
|
+
from .annotations import get_ns_annotations, is_classvar
|
|
36
|
+
|
|
37
|
+
__version__ = "v0.6.0"
|
|
25
38
|
|
|
26
39
|
# Change this name if you make heavy modifications
|
|
27
40
|
INTERNALS_DICT = "__classbuilder_internals__"
|
|
41
|
+
META_GATHERER_NAME = "_meta_gatherer"
|
|
28
42
|
|
|
29
43
|
|
|
30
44
|
# If testing, make Field classes frozen to make sure attributes are not
|
|
31
45
|
# overwritten. When running this is a performance penalty so it is not required.
|
|
32
46
|
_UNDER_TESTING = "pytest" in sys.modules
|
|
33
47
|
|
|
48
|
+
# Obtain types the same way types.py does in pypy
|
|
49
|
+
# See: https://github.com/pypy/pypy/blob/19d9fa6be11165116dd0839b9144d969ab426ae7/lib-python/3/types.py#L61-L73
|
|
50
|
+
class _C: __slots__ = 's' # noqa
|
|
51
|
+
_MemberDescriptorType = type(_C.s) # noqa
|
|
52
|
+
_MappingProxyType = type(type.__dict__)
|
|
53
|
+
del _C
|
|
54
|
+
|
|
34
55
|
|
|
35
56
|
def get_fields(cls, *, local=False):
|
|
36
57
|
"""
|
|
@@ -56,6 +77,17 @@ def get_flags(cls):
|
|
|
56
77
|
return getattr(cls, INTERNALS_DICT)["flags"]
|
|
57
78
|
|
|
58
79
|
|
|
80
|
+
def get_methods(cls):
|
|
81
|
+
"""
|
|
82
|
+
Utility function to gather the set of methods
|
|
83
|
+
from the class internals.
|
|
84
|
+
|
|
85
|
+
:param cls: generated class
|
|
86
|
+
:return: dict of generated methods attached to the class by name
|
|
87
|
+
"""
|
|
88
|
+
return getattr(cls, INTERNALS_DICT)["methods"]
|
|
89
|
+
|
|
90
|
+
|
|
59
91
|
def _get_inst_fields(inst):
|
|
60
92
|
# This is an internal helper for constructing new
|
|
61
93
|
# 'Field' instances from existing ones.
|
|
@@ -75,6 +107,38 @@ class _NothingType:
|
|
|
75
107
|
NOTHING = _NothingType()
|
|
76
108
|
|
|
77
109
|
|
|
110
|
+
# KW_ONLY sentinel 'type' to use to indicate all subsequent attributes are
|
|
111
|
+
# keyword only
|
|
112
|
+
# noinspection PyPep8Naming
|
|
113
|
+
class _KW_ONLY_TYPE:
|
|
114
|
+
def __repr__(self):
|
|
115
|
+
return "<KW_ONLY Sentinel Object>"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
KW_ONLY = _KW_ONLY_TYPE()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class GeneratedCode:
|
|
122
|
+
"""
|
|
123
|
+
This class provides a return value for the generated output from source code
|
|
124
|
+
generators.
|
|
125
|
+
"""
|
|
126
|
+
__slots__ = ("source_code", "globs")
|
|
127
|
+
|
|
128
|
+
def __init__(self, source_code, globs):
|
|
129
|
+
self.source_code = source_code
|
|
130
|
+
self.globs = globs
|
|
131
|
+
|
|
132
|
+
def __repr__(self):
|
|
133
|
+
first_source_line = self.source_code.split("\n")[0]
|
|
134
|
+
return f"GeneratorOutput(source_code='{first_source_line} ...', globs={self.globs!r})"
|
|
135
|
+
|
|
136
|
+
def __eq__(self, other):
|
|
137
|
+
if self.__class__ is other.__class__:
|
|
138
|
+
return (self.source_code, self.globs) == (other.source_code, other.globs)
|
|
139
|
+
return NotImplemented
|
|
140
|
+
|
|
141
|
+
|
|
78
142
|
class MethodMaker:
|
|
79
143
|
"""
|
|
80
144
|
The descriptor class to place where methods should be generated.
|
|
@@ -94,19 +158,32 @@ class MethodMaker:
|
|
|
94
158
|
def __repr__(self):
|
|
95
159
|
return f"<MethodMaker for {self.funcname!r} method>"
|
|
96
160
|
|
|
97
|
-
def __get__(self,
|
|
161
|
+
def __get__(self, obj, objtype=None):
|
|
162
|
+
if objtype is None or issubclass(objtype, type):
|
|
163
|
+
# Called with get(ourclass, type(ourclass))
|
|
164
|
+
cls = obj
|
|
165
|
+
else:
|
|
166
|
+
# Called with get(inst | None, ourclass)
|
|
167
|
+
cls = objtype
|
|
168
|
+
|
|
98
169
|
local_vars = {}
|
|
99
|
-
|
|
100
|
-
exec(
|
|
170
|
+
gen = self.code_generator(cls)
|
|
171
|
+
exec(gen.source_code, gen.globs, local_vars)
|
|
101
172
|
method = local_vars.get(self.funcname)
|
|
102
|
-
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
method.__qualname__ = f"{cls.__qualname__}.{self.funcname}"
|
|
176
|
+
except AttributeError:
|
|
177
|
+
# This might be a property or some other special
|
|
178
|
+
# descriptor. Don't try to rename.
|
|
179
|
+
pass
|
|
103
180
|
|
|
104
181
|
# Replace this descriptor on the class with the generated function
|
|
105
182
|
setattr(cls, self.funcname, method)
|
|
106
183
|
|
|
107
184
|
# Use 'get' to return the generated function as a bound method
|
|
108
185
|
# instead of as a regular function for first usage.
|
|
109
|
-
return method.__get__(
|
|
186
|
+
return method.__get__(obj, objtype)
|
|
110
187
|
|
|
111
188
|
|
|
112
189
|
def get_init_generator(null=NOTHING, extra_code=None):
|
|
@@ -115,29 +192,51 @@ def get_init_generator(null=NOTHING, extra_code=None):
|
|
|
115
192
|
flags = get_flags(cls)
|
|
116
193
|
|
|
117
194
|
arglist = []
|
|
195
|
+
kw_only_arglist = []
|
|
118
196
|
assignments = []
|
|
119
197
|
globs = {}
|
|
120
198
|
|
|
121
|
-
|
|
122
|
-
arglist.append("*")
|
|
199
|
+
kw_only_flag = flags.get("kw_only", False)
|
|
123
200
|
|
|
124
201
|
for k, v in fields.items():
|
|
125
|
-
if v.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
202
|
+
if v.init:
|
|
203
|
+
if v.default is not null:
|
|
204
|
+
globs[f"_{k}_default"] = v.default
|
|
205
|
+
arg = f"{k}=_{k}_default"
|
|
206
|
+
assignment = f"self.{k} = {k}"
|
|
207
|
+
elif v.default_factory is not null:
|
|
208
|
+
globs[f"_{k}_factory"] = v.default_factory
|
|
209
|
+
arg = f"{k}=None"
|
|
210
|
+
assignment = f"self.{k} = _{k}_factory() if {k} is None else {k}"
|
|
211
|
+
else:
|
|
212
|
+
arg = f"{k}"
|
|
213
|
+
assignment = f"self.{k} = {k}"
|
|
214
|
+
|
|
215
|
+
if kw_only_flag or v.kw_only:
|
|
216
|
+
kw_only_arglist.append(arg)
|
|
217
|
+
else:
|
|
218
|
+
arglist.append(arg)
|
|
136
219
|
|
|
137
|
-
|
|
138
|
-
|
|
220
|
+
assignments.append(assignment)
|
|
221
|
+
else:
|
|
222
|
+
if v.default is not null:
|
|
223
|
+
globs[f"_{k}_default"] = v.default
|
|
224
|
+
assignment = f"self.{k} = _{k}_default"
|
|
225
|
+
assignments.append(assignment)
|
|
226
|
+
elif v.default_factory is not null:
|
|
227
|
+
globs[f"_{k}_factory"] = v.default_factory
|
|
228
|
+
assignment = f"self.{k} = _{k}_factory()"
|
|
229
|
+
assignments.append(assignment)
|
|
230
|
+
|
|
231
|
+
pos_args = ", ".join(arglist)
|
|
232
|
+
kw_args = ", ".join(kw_only_arglist)
|
|
233
|
+
if pos_args and kw_args:
|
|
234
|
+
args = f"{pos_args}, *, {kw_args}"
|
|
235
|
+
elif kw_args:
|
|
236
|
+
args = f"*, {kw_args}"
|
|
237
|
+
else:
|
|
238
|
+
args = pos_args
|
|
139
239
|
|
|
140
|
-
args = ", ".join(arglist)
|
|
141
240
|
assigns = "\n ".join(assignments) if assignments else "pass\n"
|
|
142
241
|
code = (
|
|
143
242
|
f"def __init__(self, {args}):\n"
|
|
@@ -149,7 +248,7 @@ def get_init_generator(null=NOTHING, extra_code=None):
|
|
|
149
248
|
for line in extra_code:
|
|
150
249
|
code += f" {line}\n"
|
|
151
250
|
|
|
152
|
-
return code, globs
|
|
251
|
+
return GeneratedCode(code, globs)
|
|
153
252
|
|
|
154
253
|
return cls_init_maker
|
|
155
254
|
|
|
@@ -157,23 +256,75 @@ def get_init_generator(null=NOTHING, extra_code=None):
|
|
|
157
256
|
init_generator = get_init_generator()
|
|
158
257
|
|
|
159
258
|
|
|
160
|
-
def
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
|
|
259
|
+
def get_repr_generator(recursion_safe=False, eval_safe=False):
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
:param recursion_safe: use reprlib.recursive_repr
|
|
263
|
+
:param eval_safe: if the repr is known not to eval correctly,
|
|
264
|
+
generate a repr which will intentionally
|
|
265
|
+
not evaluate.
|
|
266
|
+
:return:
|
|
267
|
+
"""
|
|
268
|
+
def cls_repr_generator(cls):
|
|
269
|
+
fields = get_fields(cls)
|
|
270
|
+
|
|
271
|
+
globs = {}
|
|
272
|
+
will_eval = True
|
|
273
|
+
valid_names = []
|
|
274
|
+
|
|
275
|
+
for name, fld in fields.items():
|
|
276
|
+
if fld.repr:
|
|
277
|
+
valid_names.append(name)
|
|
278
|
+
|
|
279
|
+
if will_eval and (fld.init ^ fld.repr):
|
|
280
|
+
will_eval = False
|
|
281
|
+
|
|
282
|
+
content = ", ".join(
|
|
283
|
+
f"{name}={{self.{name}!r}}"
|
|
284
|
+
for name in valid_names
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if recursion_safe:
|
|
288
|
+
import reprlib
|
|
289
|
+
globs["_recursive_repr"] = reprlib.recursive_repr()
|
|
290
|
+
recursion_func = "@_recursive_repr\n"
|
|
291
|
+
else:
|
|
292
|
+
recursion_func = ""
|
|
293
|
+
|
|
294
|
+
if eval_safe and will_eval is False:
|
|
295
|
+
if content:
|
|
296
|
+
code = (
|
|
297
|
+
f"{recursion_func}"
|
|
298
|
+
f"def __repr__(self):\n"
|
|
299
|
+
f" return f'<generated class {{type(self).__qualname__}}; {content}>'\n"
|
|
300
|
+
)
|
|
301
|
+
else:
|
|
302
|
+
code = (
|
|
303
|
+
f"{recursion_func}"
|
|
304
|
+
f"def __repr__(self):\n"
|
|
305
|
+
f" return f'<generated class {{type(self).__qualname__}}>'\n"
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
code = (
|
|
309
|
+
f"{recursion_func}"
|
|
310
|
+
f"def __repr__(self):\n"
|
|
311
|
+
f" return f'{{type(self).__qualname__}}({content})'\n"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return GeneratedCode(code, globs)
|
|
315
|
+
return cls_repr_generator
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
repr_generator = get_repr_generator()
|
|
172
319
|
|
|
173
320
|
|
|
174
321
|
def eq_generator(cls):
|
|
175
322
|
class_comparison = "self.__class__ is other.__class__"
|
|
176
|
-
field_names =
|
|
323
|
+
field_names = [
|
|
324
|
+
name
|
|
325
|
+
for name, attrib in get_fields(cls).items()
|
|
326
|
+
if attrib.compare
|
|
327
|
+
]
|
|
177
328
|
|
|
178
329
|
if field_names:
|
|
179
330
|
selfvals = ",".join(f"self.{name}" for name in field_names)
|
|
@@ -188,7 +339,7 @@ def eq_generator(cls):
|
|
|
188
339
|
)
|
|
189
340
|
globs = {}
|
|
190
341
|
|
|
191
|
-
return code, globs
|
|
342
|
+
return GeneratedCode(code, globs)
|
|
192
343
|
|
|
193
344
|
|
|
194
345
|
def frozen_setattr_generator(cls):
|
|
@@ -217,7 +368,7 @@ def frozen_setattr_generator(cls):
|
|
|
217
368
|
)
|
|
218
369
|
code = f"def __setattr__(self, name, value):\n{body}"
|
|
219
370
|
|
|
220
|
-
return code, globs
|
|
371
|
+
return GeneratedCode(code, globs)
|
|
221
372
|
|
|
222
373
|
|
|
223
374
|
def frozen_delattr_generator(cls):
|
|
@@ -229,7 +380,7 @@ def frozen_delattr_generator(cls):
|
|
|
229
380
|
)
|
|
230
381
|
code = f"def __delattr__(self, name):\n{body}"
|
|
231
382
|
globs = {}
|
|
232
|
-
return code, globs
|
|
383
|
+
return GeneratedCode(code, globs)
|
|
233
384
|
|
|
234
385
|
|
|
235
386
|
# As only the __get__ method refers to the class we can use the same
|
|
@@ -241,6 +392,15 @@ frozen_setattr_maker = MethodMaker("__setattr__", frozen_setattr_generator)
|
|
|
241
392
|
frozen_delattr_maker = MethodMaker("__delattr__", frozen_delattr_generator)
|
|
242
393
|
default_methods = frozenset({init_maker, repr_maker, eq_maker})
|
|
243
394
|
|
|
395
|
+
# Special `__init__` maker for 'Field' subclasses
|
|
396
|
+
_field_init_maker = MethodMaker(
|
|
397
|
+
funcname="__init__",
|
|
398
|
+
code_generator=get_init_generator(
|
|
399
|
+
null=_NothingType(),
|
|
400
|
+
extra_code=["self.validate_field()"],
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
|
|
244
404
|
|
|
245
405
|
def builder(cls=None, /, *, gatherer, methods, flags=None):
|
|
246
406
|
"""
|
|
@@ -292,16 +452,77 @@ def builder(cls=None, /, *, gatherer, methods, flags=None):
|
|
|
292
452
|
internals["flags"] = flags if flags is not None else {}
|
|
293
453
|
|
|
294
454
|
# Assign all of the method generators
|
|
455
|
+
internal_methods = {}
|
|
295
456
|
for method in methods:
|
|
296
457
|
setattr(cls, method.funcname, method)
|
|
458
|
+
internal_methods[method.funcname] = method
|
|
459
|
+
|
|
460
|
+
internals["methods"] = _MappingProxyType(internal_methods)
|
|
297
461
|
|
|
298
462
|
return cls
|
|
299
463
|
|
|
300
464
|
|
|
465
|
+
# Slot gathering tools
|
|
466
|
+
# Subclass of dict to be identifiable by isinstance checks
|
|
467
|
+
# For anything more complicated this could be made into a Mapping
|
|
468
|
+
class SlotFields(dict):
|
|
469
|
+
"""
|
|
470
|
+
A plain dict subclass.
|
|
471
|
+
|
|
472
|
+
For declaring slotfields there are no additional features required
|
|
473
|
+
other than recognising that this is intended to be used as a class
|
|
474
|
+
generating dict and isn't a regular dictionary that ended up in
|
|
475
|
+
`__slots__`.
|
|
476
|
+
|
|
477
|
+
This should be replaced on `__slots__` after fields have been gathered.
|
|
478
|
+
"""
|
|
479
|
+
def __repr__(self):
|
|
480
|
+
return f"SlotFields({super().__repr__()})"
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# Tool to convert annotations to slots as a metaclass
|
|
484
|
+
class SlotMakerMeta(type):
|
|
485
|
+
"""
|
|
486
|
+
Metaclass to convert annotations or Field(...) attributes to slots.
|
|
487
|
+
|
|
488
|
+
Will not convert `ClassVar` hinted values.
|
|
489
|
+
"""
|
|
490
|
+
def __new__(cls, name, bases, ns, slots=True, **kwargs):
|
|
491
|
+
# This should only run if slots=True is declared
|
|
492
|
+
# and __slots__ have not already been defined
|
|
493
|
+
if slots and "__slots__" not in ns:
|
|
494
|
+
# Check if a different gatherer has been set in any base classes
|
|
495
|
+
# Default to unified gatherer
|
|
496
|
+
gatherer = ns.get(META_GATHERER_NAME, None)
|
|
497
|
+
if not gatherer:
|
|
498
|
+
for base in bases:
|
|
499
|
+
if g := getattr(base, META_GATHERER_NAME, None):
|
|
500
|
+
gatherer = g
|
|
501
|
+
break
|
|
502
|
+
|
|
503
|
+
if not gatherer:
|
|
504
|
+
gatherer = unified_gatherer
|
|
505
|
+
|
|
506
|
+
# Obtain slots from annotations or attributes
|
|
507
|
+
cls_fields, cls_modifications = gatherer(ns)
|
|
508
|
+
for k, v in cls_modifications.items():
|
|
509
|
+
if v is NOTHING:
|
|
510
|
+
ns.pop(k)
|
|
511
|
+
else:
|
|
512
|
+
ns[k] = v
|
|
513
|
+
|
|
514
|
+
# Place slots *after* everything else to be safe
|
|
515
|
+
ns["__slots__"] = SlotFields(cls_fields)
|
|
516
|
+
|
|
517
|
+
new_cls = super().__new__(cls, name, bases, ns, **kwargs)
|
|
518
|
+
|
|
519
|
+
return new_cls
|
|
520
|
+
|
|
521
|
+
|
|
301
522
|
# The Field class can finally be defined.
|
|
302
523
|
# The __init__ method has to be written manually so Fields can be created
|
|
303
524
|
# However after this, the other methods can be generated.
|
|
304
|
-
class Field:
|
|
525
|
+
class Field(metaclass=SlotMakerMeta):
|
|
305
526
|
"""
|
|
306
527
|
A basic class to handle the assignment of defaults/factories with
|
|
307
528
|
some metadata.
|
|
@@ -309,16 +530,32 @@ class Field:
|
|
|
309
530
|
Intended to be extendable by subclasses for additional features.
|
|
310
531
|
|
|
311
532
|
Note: When run under `pytest`, Field instances are Frozen.
|
|
533
|
+
|
|
534
|
+
When subclassing, passing `frozen=True` will make your subclass frozen.
|
|
535
|
+
|
|
536
|
+
:param default: Standard default value to be used for attributes with this field.
|
|
537
|
+
:param default_factory: A zero-argument function to be called to generate a
|
|
538
|
+
default value, useful for mutable obects like lists.
|
|
539
|
+
:param type: The type of the attribute to be assigned by this field.
|
|
540
|
+
:param doc: The documentation for the attribute that appears when calling
|
|
541
|
+
help(...) on the class. (Only in slotted classes).
|
|
542
|
+
:param init: Include in the class __init__ parameters.
|
|
543
|
+
:param repr: Include in the class __repr__.
|
|
544
|
+
:param compare: Include in the class __eq__.
|
|
545
|
+
:param kw_only: Make this a keyword only parameter in __init__.
|
|
312
546
|
"""
|
|
313
|
-
__slots__
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
547
|
+
# If this base class did not define __slots__ the metaclass would break it.
|
|
548
|
+
# This will be replaced by the builder.
|
|
549
|
+
__slots__ = SlotFields(
|
|
550
|
+
default=NOTHING,
|
|
551
|
+
default_factory=NOTHING,
|
|
552
|
+
type=NOTHING,
|
|
553
|
+
doc=None,
|
|
554
|
+
init=True,
|
|
555
|
+
repr=True,
|
|
556
|
+
compare=True,
|
|
557
|
+
kw_only=False,
|
|
558
|
+
)
|
|
322
559
|
|
|
323
560
|
# noinspection PyShadowingBuiltins
|
|
324
561
|
def __init__(
|
|
@@ -328,18 +565,50 @@ class Field:
|
|
|
328
565
|
default_factory=NOTHING,
|
|
329
566
|
type=NOTHING,
|
|
330
567
|
doc=None,
|
|
568
|
+
init=True,
|
|
569
|
+
repr=True,
|
|
570
|
+
compare=True,
|
|
571
|
+
kw_only=False,
|
|
331
572
|
):
|
|
573
|
+
# The init function for 'Field' cannot be generated
|
|
574
|
+
# as 'Field' needs to exist first.
|
|
575
|
+
# repr and comparison functions are generated as these
|
|
576
|
+
# do not need to exist to create initial Fields.
|
|
577
|
+
|
|
332
578
|
self.default = default
|
|
333
579
|
self.default_factory = default_factory
|
|
334
580
|
self.type = type
|
|
335
581
|
self.doc = doc
|
|
336
582
|
|
|
583
|
+
self.init = init
|
|
584
|
+
self.repr = repr
|
|
585
|
+
self.compare = compare
|
|
586
|
+
self.kw_only = kw_only
|
|
587
|
+
|
|
337
588
|
self.validate_field()
|
|
338
589
|
|
|
590
|
+
def __init_subclass__(cls, frozen=False):
|
|
591
|
+
field_methods = {_field_init_maker, repr_maker, eq_maker}
|
|
592
|
+
if frozen or _UNDER_TESTING:
|
|
593
|
+
field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
594
|
+
|
|
595
|
+
builder(
|
|
596
|
+
cls,
|
|
597
|
+
gatherer=unified_gatherer,
|
|
598
|
+
methods=field_methods,
|
|
599
|
+
flags={"slotted": True, "kw_only": True}
|
|
600
|
+
)
|
|
601
|
+
|
|
339
602
|
def validate_field(self):
|
|
603
|
+
cls_name = self.__class__.__name__
|
|
340
604
|
if self.default is not NOTHING and self.default_factory is not NOTHING:
|
|
341
605
|
raise AttributeError(
|
|
342
|
-
"
|
|
606
|
+
f"{cls_name} cannot define both a default value and a default factory."
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
if self.kw_only and not self.init:
|
|
610
|
+
raise AttributeError(
|
|
611
|
+
f"{cls_name} cannot be keyword only if it is not in init."
|
|
343
612
|
)
|
|
344
613
|
|
|
345
614
|
@classmethod
|
|
@@ -359,66 +628,6 @@ class Field:
|
|
|
359
628
|
return cls(**argument_dict)
|
|
360
629
|
|
|
361
630
|
|
|
362
|
-
class GatheredFields:
|
|
363
|
-
__slots__ = ("fields", "modifications")
|
|
364
|
-
|
|
365
|
-
def __init__(self, fields, modifications):
|
|
366
|
-
self.fields = fields
|
|
367
|
-
self.modifications = modifications
|
|
368
|
-
|
|
369
|
-
def __call__(self, cls):
|
|
370
|
-
return self.fields, self.modifications
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
# Use the builder to generate __repr__ and __eq__ methods
|
|
374
|
-
# for both Field and GatheredFields
|
|
375
|
-
_field_internal = {
|
|
376
|
-
"default": Field(default=NOTHING),
|
|
377
|
-
"default_factory": Field(default=NOTHING),
|
|
378
|
-
"type": Field(default=NOTHING),
|
|
379
|
-
"doc": Field(default=None),
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
_gathered_field_internal = {
|
|
383
|
-
"fields": Field(default=NOTHING),
|
|
384
|
-
"modifications": Field(default=NOTHING),
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
_field_methods = {repr_maker, eq_maker}
|
|
388
|
-
if _UNDER_TESTING:
|
|
389
|
-
_field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
390
|
-
|
|
391
|
-
builder(
|
|
392
|
-
Field,
|
|
393
|
-
gatherer=GatheredFields(_field_internal, {}),
|
|
394
|
-
methods=_field_methods,
|
|
395
|
-
flags={"slotted": True, "kw_only": True},
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
builder(
|
|
399
|
-
GatheredFields,
|
|
400
|
-
gatherer=GatheredFields(_gathered_field_internal, {}),
|
|
401
|
-
methods={repr_maker, eq_maker},
|
|
402
|
-
flags={"slotted": True, "kw_only": False},
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
# Slot gathering tools
|
|
407
|
-
# Subclass of dict to be identifiable by isinstance checks
|
|
408
|
-
# For anything more complicated this could be made into a Mapping
|
|
409
|
-
class SlotFields(dict):
|
|
410
|
-
"""
|
|
411
|
-
A plain dict subclass.
|
|
412
|
-
|
|
413
|
-
For declaring slotfields there are no additional features required
|
|
414
|
-
other than recognising that this is intended to be used as a class
|
|
415
|
-
generating dict and isn't a regular dictionary that ended up in
|
|
416
|
-
`__slots__`.
|
|
417
|
-
|
|
418
|
-
This should be replaced on `__slots__` after fields have been gathered.
|
|
419
|
-
"""
|
|
420
|
-
|
|
421
|
-
|
|
422
631
|
def make_slot_gatherer(field_type=Field):
|
|
423
632
|
"""
|
|
424
633
|
Create a new annotation gatherer that will work with `Field` instances
|
|
@@ -428,14 +637,25 @@ def make_slot_gatherer(field_type=Field):
|
|
|
428
637
|
:return: A slot gatherer that will check for and generate Fields of
|
|
429
638
|
the type field_type.
|
|
430
639
|
"""
|
|
431
|
-
def field_slot_gatherer(
|
|
640
|
+
def field_slot_gatherer(cls_or_ns):
|
|
432
641
|
"""
|
|
433
642
|
Gather field information for class generation based on __slots__
|
|
434
643
|
|
|
435
|
-
:param
|
|
644
|
+
:param cls_or_ns: Class to gather field information from (or class namespace)
|
|
436
645
|
:return: dict of field_name: Field(...)
|
|
437
646
|
"""
|
|
438
|
-
|
|
647
|
+
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
648
|
+
cls_dict = cls_or_ns
|
|
649
|
+
else:
|
|
650
|
+
cls_dict = cls_or_ns.__dict__
|
|
651
|
+
|
|
652
|
+
try:
|
|
653
|
+
cls_slots = cls_dict["__slots__"]
|
|
654
|
+
except KeyError:
|
|
655
|
+
raise AttributeError(
|
|
656
|
+
"__slots__ must be defined as an instance of SlotFields "
|
|
657
|
+
"in order to generate a slotclass"
|
|
658
|
+
)
|
|
439
659
|
|
|
440
660
|
if not isinstance(cls_slots, SlotFields):
|
|
441
661
|
raise TypeError(
|
|
@@ -445,9 +665,7 @@ def make_slot_gatherer(field_type=Field):
|
|
|
445
665
|
|
|
446
666
|
# Don't want to mutate original annotations so make a copy if it exists
|
|
447
667
|
# Looking at the dict is a Python3.9 or earlier requirement
|
|
448
|
-
cls_annotations =
|
|
449
|
-
**cls.__dict__.get("__annotations__", {})
|
|
450
|
-
}
|
|
668
|
+
cls_annotations = get_ns_annotations(cls_dict)
|
|
451
669
|
|
|
452
670
|
cls_fields = {}
|
|
453
671
|
slot_replacement = {}
|
|
@@ -484,43 +702,10 @@ def make_slot_gatherer(field_type=Field):
|
|
|
484
702
|
return field_slot_gatherer
|
|
485
703
|
|
|
486
704
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
def is_classvar(hint):
|
|
492
|
-
_typing = sys.modules.get("typing")
|
|
493
|
-
|
|
494
|
-
if _typing:
|
|
495
|
-
# Annotated is a nightmare I'm never waking up from
|
|
496
|
-
# 3.8 and 3.9 need Annotated from typing_extensions
|
|
497
|
-
# 3.8 also needs get_origin from typing_extensions
|
|
498
|
-
if sys.version_info < (3, 10):
|
|
499
|
-
_typing_extensions = sys.modules.get("typing_extensions")
|
|
500
|
-
if _typing_extensions:
|
|
501
|
-
_Annotated = _typing_extensions.Annotated
|
|
502
|
-
_get_origin = _typing_extensions.get_origin
|
|
503
|
-
else:
|
|
504
|
-
_Annotated, _get_origin = None, None
|
|
505
|
-
else:
|
|
506
|
-
_Annotated = _typing.Annotated
|
|
507
|
-
_get_origin = _typing.get_origin
|
|
508
|
-
|
|
509
|
-
if _Annotated and _get_origin(hint) is _Annotated:
|
|
510
|
-
hint = getattr(hint, "__origin__", None)
|
|
511
|
-
|
|
512
|
-
if (
|
|
513
|
-
hint is _typing.ClassVar
|
|
514
|
-
or getattr(hint, "__origin__", None) is _typing.ClassVar
|
|
515
|
-
):
|
|
516
|
-
return True
|
|
517
|
-
# String used as annotation
|
|
518
|
-
elif isinstance(hint, str) and "ClassVar" in hint:
|
|
519
|
-
return True
|
|
520
|
-
return False
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
def make_annotation_gatherer(field_type=Field, leave_default_values=True):
|
|
705
|
+
def make_annotation_gatherer(
|
|
706
|
+
field_type=Field,
|
|
707
|
+
leave_default_values=True,
|
|
708
|
+
):
|
|
524
709
|
"""
|
|
525
710
|
Create a new annotation gatherer that will work with `Field` instances
|
|
526
711
|
of the creators definition.
|
|
@@ -530,35 +715,51 @@ def make_annotation_gatherer(field_type=Field, leave_default_values=True):
|
|
|
530
715
|
default values in place as class variables.
|
|
531
716
|
:return: An annotation gatherer with these settings.
|
|
532
717
|
"""
|
|
533
|
-
def field_annotation_gatherer(
|
|
534
|
-
|
|
718
|
+
def field_annotation_gatherer(cls_or_ns):
|
|
719
|
+
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
720
|
+
cls_dict = cls_or_ns
|
|
721
|
+
else:
|
|
722
|
+
cls_dict = cls_or_ns.__dict__
|
|
535
723
|
|
|
536
724
|
cls_fields: dict[str, field_type] = {}
|
|
537
|
-
|
|
538
725
|
modifications = {}
|
|
539
726
|
|
|
727
|
+
cls_annotations = get_ns_annotations(cls_dict)
|
|
728
|
+
|
|
729
|
+
kw_flag = False
|
|
730
|
+
|
|
540
731
|
for k, v in cls_annotations.items():
|
|
541
732
|
# Ignore ClassVar
|
|
542
733
|
if is_classvar(v):
|
|
543
734
|
continue
|
|
544
735
|
|
|
545
|
-
|
|
736
|
+
if v is KW_ONLY:
|
|
737
|
+
if kw_flag:
|
|
738
|
+
raise SyntaxError("KW_ONLY sentinel may only appear once.")
|
|
739
|
+
kw_flag = True
|
|
740
|
+
continue
|
|
741
|
+
|
|
742
|
+
attrib = cls_dict.get(k, NOTHING)
|
|
546
743
|
|
|
547
744
|
if attrib is not NOTHING:
|
|
548
745
|
if isinstance(attrib, field_type):
|
|
549
|
-
|
|
746
|
+
kw_only = attrib.kw_only or kw_flag
|
|
747
|
+
|
|
748
|
+
attrib = field_type.from_field(attrib, type=v, kw_only=kw_only)
|
|
749
|
+
|
|
550
750
|
if attrib.default is not NOTHING and leave_default_values:
|
|
551
751
|
modifications[k] = attrib.default
|
|
552
752
|
else:
|
|
553
753
|
# NOTHING sentinel indicates a value should be removed
|
|
554
754
|
modifications[k] = NOTHING
|
|
555
|
-
|
|
556
|
-
attrib = field_type(default=attrib, type=v)
|
|
755
|
+
elif not isinstance(attrib, _MemberDescriptorType):
|
|
756
|
+
attrib = field_type(default=attrib, type=v, kw_only=kw_flag)
|
|
557
757
|
if not leave_default_values:
|
|
558
758
|
modifications[k] = NOTHING
|
|
559
|
-
|
|
759
|
+
else:
|
|
760
|
+
attrib = field_type(type=v, kw_only=kw_flag)
|
|
560
761
|
else:
|
|
561
|
-
attrib = field_type(type=v)
|
|
762
|
+
attrib = field_type(type=v, kw_only=kw_flag)
|
|
562
763
|
|
|
563
764
|
cls_fields[k] = attrib
|
|
564
765
|
|
|
@@ -567,8 +768,106 @@ def make_annotation_gatherer(field_type=Field, leave_default_values=True):
|
|
|
567
768
|
return field_annotation_gatherer
|
|
568
769
|
|
|
569
770
|
|
|
771
|
+
def make_field_gatherer(
|
|
772
|
+
field_type=Field,
|
|
773
|
+
leave_default_values=True,
|
|
774
|
+
):
|
|
775
|
+
def field_attribute_gatherer(cls_or_ns):
|
|
776
|
+
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
777
|
+
cls_dict = cls_or_ns
|
|
778
|
+
else:
|
|
779
|
+
cls_dict = cls_or_ns.__dict__
|
|
780
|
+
|
|
781
|
+
cls_attributes = {
|
|
782
|
+
k: v
|
|
783
|
+
for k, v in cls_dict.items()
|
|
784
|
+
if isinstance(v, field_type)
|
|
785
|
+
}
|
|
786
|
+
cls_annotations = get_ns_annotations(cls_dict)
|
|
787
|
+
|
|
788
|
+
cls_modifications = {}
|
|
789
|
+
|
|
790
|
+
for name in cls_attributes.keys():
|
|
791
|
+
attrib = cls_attributes[name]
|
|
792
|
+
if leave_default_values:
|
|
793
|
+
cls_modifications[name] = attrib.default
|
|
794
|
+
else:
|
|
795
|
+
cls_modifications[name] = NOTHING
|
|
796
|
+
|
|
797
|
+
if (anno := cls_annotations.get(name, NOTHING)) is not NOTHING:
|
|
798
|
+
cls_attributes[name] = field_type.from_field(attrib, type=anno)
|
|
799
|
+
|
|
800
|
+
return cls_attributes, cls_modifications
|
|
801
|
+
return field_attribute_gatherer
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def make_unified_gatherer(
|
|
805
|
+
field_type=Field,
|
|
806
|
+
leave_default_values=True,
|
|
807
|
+
):
|
|
808
|
+
"""
|
|
809
|
+
Create a gatherer that will work via first slots, then
|
|
810
|
+
Field(...) class attributes and finally annotations if
|
|
811
|
+
no unannotated Field(...) attributes are present.
|
|
812
|
+
|
|
813
|
+
:param field_type: The field class to use for gathering
|
|
814
|
+
:param leave_default_values: leave default values in place
|
|
815
|
+
:return: gatherer function
|
|
816
|
+
"""
|
|
817
|
+
slot_g = make_slot_gatherer(field_type)
|
|
818
|
+
anno_g = make_annotation_gatherer(field_type, leave_default_values)
|
|
819
|
+
attrib_g = make_field_gatherer(field_type, leave_default_values)
|
|
820
|
+
|
|
821
|
+
def field_unified_gatherer(cls_or_ns):
|
|
822
|
+
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
823
|
+
cls_dict = cls_or_ns
|
|
824
|
+
else:
|
|
825
|
+
cls_dict = cls_or_ns.__dict__
|
|
826
|
+
|
|
827
|
+
cls_slots = cls_dict.get("__slots__")
|
|
828
|
+
|
|
829
|
+
if isinstance(cls_slots, SlotFields):
|
|
830
|
+
return slot_g(cls_dict)
|
|
831
|
+
|
|
832
|
+
# To choose between annotation and attribute gatherers
|
|
833
|
+
# compare sets of names.
|
|
834
|
+
# Don't bother evaluating string annotations, as we only need names
|
|
835
|
+
cls_annotations = get_ns_annotations(cls_dict, eval_str=False)
|
|
836
|
+
cls_attributes = {
|
|
837
|
+
k: v for k, v in cls_dict.items() if isinstance(v, field_type)
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
cls_annotation_names = cls_annotations.keys()
|
|
841
|
+
cls_attribute_names = cls_attributes.keys()
|
|
842
|
+
|
|
843
|
+
if set(cls_annotation_names).issuperset(set(cls_attribute_names)):
|
|
844
|
+
# All `Field` values have annotations, so use annotation gatherer
|
|
845
|
+
return anno_g(cls_dict)
|
|
846
|
+
|
|
847
|
+
return attrib_g(cls_dict)
|
|
848
|
+
return field_unified_gatherer
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
slot_gatherer = make_slot_gatherer()
|
|
570
852
|
annotation_gatherer = make_annotation_gatherer()
|
|
571
853
|
|
|
854
|
+
# The unified gatherer used for slot classes must remove default
|
|
855
|
+
# values for slots to work correctly.
|
|
856
|
+
unified_gatherer = make_unified_gatherer(leave_default_values=False)
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
# Now the gatherers have been defined, add __repr__ and __eq__ to Field.
|
|
860
|
+
_field_methods = {repr_maker, eq_maker}
|
|
861
|
+
if _UNDER_TESTING:
|
|
862
|
+
_field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
863
|
+
|
|
864
|
+
builder(
|
|
865
|
+
Field,
|
|
866
|
+
gatherer=slot_gatherer,
|
|
867
|
+
methods=_field_methods,
|
|
868
|
+
flags={"slotted": True, "kw_only": True},
|
|
869
|
+
)
|
|
870
|
+
|
|
572
871
|
|
|
573
872
|
def check_argument_order(cls):
|
|
574
873
|
"""
|
|
@@ -580,6 +879,9 @@ def check_argument_order(cls):
|
|
|
580
879
|
fields = get_fields(cls)
|
|
581
880
|
used_default = False
|
|
582
881
|
for k, v in fields.items():
|
|
882
|
+
if v.kw_only or (not v.init):
|
|
883
|
+
continue
|
|
884
|
+
|
|
583
885
|
if v.default is NOTHING and v.default_factory is NOTHING:
|
|
584
886
|
if used_default:
|
|
585
887
|
raise SyntaxError(
|
|
@@ -611,51 +913,33 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
|
|
|
611
913
|
return cls
|
|
612
914
|
|
|
613
915
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
return lambda cls_: annotationclass(cls_, methods=methods)
|
|
617
|
-
|
|
618
|
-
cls = builder(cls, gatherer=annotation_gatherer, methods=methods, flags={"slotted": False})
|
|
619
|
-
|
|
620
|
-
check_argument_order(cls)
|
|
621
|
-
|
|
622
|
-
return cls
|
|
916
|
+
class AnnotationClass(metaclass=SlotMakerMeta):
|
|
917
|
+
__slots__ = {}
|
|
623
918
|
|
|
919
|
+
def __init_subclass__(
|
|
920
|
+
cls,
|
|
921
|
+
methods=default_methods,
|
|
922
|
+
gatherer=make_unified_gatherer(leave_default_values=True),
|
|
923
|
+
**kwargs
|
|
924
|
+
):
|
|
925
|
+
# Check class dict otherwise this will always be True as this base
|
|
926
|
+
# class uses slots.
|
|
927
|
+
slots = "__slots__" in cls.__dict__
|
|
624
928
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
null=_NothingType(),
|
|
629
|
-
extra_code=["self.validate_field()"],
|
|
630
|
-
)
|
|
631
|
-
)
|
|
929
|
+
builder(cls, gatherer=gatherer, methods=methods, flags={"slotted": slots})
|
|
930
|
+
check_argument_order(cls)
|
|
931
|
+
super().__init_subclass__(**kwargs)
|
|
632
932
|
|
|
633
933
|
|
|
634
|
-
|
|
934
|
+
@slotclass
|
|
935
|
+
class GatheredFields:
|
|
635
936
|
"""
|
|
636
|
-
|
|
637
|
-
This works by forcing the __init__ method to treat NOTHING as a regular
|
|
638
|
-
value. This means *all* instance attributes always have defaults.
|
|
639
|
-
|
|
640
|
-
:param cls: Field subclass
|
|
641
|
-
:param frozen: Make the field class a frozen class.
|
|
642
|
-
Field classes are always frozen when running under `pytest`
|
|
643
|
-
:return: Modified subclass
|
|
937
|
+
A helper gatherer for fields that have been gathered externally.
|
|
644
938
|
"""
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
field_methods = {_field_init_desc, repr_maker, eq_maker}
|
|
649
|
-
|
|
650
|
-
# Always freeze when running tests
|
|
651
|
-
if frozen or _UNDER_TESTING:
|
|
652
|
-
field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
653
|
-
|
|
654
|
-
cls = builder(
|
|
655
|
-
cls,
|
|
656
|
-
gatherer=slot_gatherer,
|
|
657
|
-
methods=field_methods,
|
|
658
|
-
flags={"slotted": True, "kw_only": True}
|
|
939
|
+
__slots__ = SlotFields(
|
|
940
|
+
fields=Field(),
|
|
941
|
+
modifications=Field(),
|
|
659
942
|
)
|
|
660
943
|
|
|
661
|
-
|
|
944
|
+
def __call__(self, cls):
|
|
945
|
+
return self.fields, self.modifications
|