ducktools-classbuilder 0.5.0__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 +493 -179
- ducktools/classbuilder/__init__.pyi +143 -39
- ducktools/classbuilder/annotations.py +173 -0
- ducktools/classbuilder/annotations.pyi +26 -0
- ducktools/classbuilder/prefab.py +120 -230
- ducktools/classbuilder/prefab.pyi +28 -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.0.dist-info/METADATA +0 -270
- ducktools_classbuilder-0.5.0.dist-info/RECORD +0 -10
- {ducktools_classbuilder-0.5.0.dist-info → ducktools_classbuilder-0.6.0.dist-info}/LICENSE.md +0 -0
- {ducktools_classbuilder-0.5.0.dist-info → ducktools_classbuilder-0.6.0.dist-info}/WHEEL +0 -0
- {ducktools_classbuilder-0.5.0.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}"
|
|
136
214
|
|
|
137
|
-
|
|
138
|
-
|
|
215
|
+
if kw_only_flag or v.kw_only:
|
|
216
|
+
kw_only_arglist.append(arg)
|
|
217
|
+
else:
|
|
218
|
+
arglist.append(arg)
|
|
219
|
+
|
|
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,43 +628,6 @@ class Field:
|
|
|
359
628
|
return cls(**argument_dict)
|
|
360
629
|
|
|
361
630
|
|
|
362
|
-
# Use the builder to generate __repr__ and __eq__ methods
|
|
363
|
-
# and pretend `Field` was a built class all along.
|
|
364
|
-
_field_internal = {
|
|
365
|
-
"default": Field(default=NOTHING),
|
|
366
|
-
"default_factory": Field(default=NOTHING),
|
|
367
|
-
"type": Field(default=NOTHING),
|
|
368
|
-
"doc": Field(default=None),
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
_field_methods = {repr_maker, eq_maker}
|
|
372
|
-
if _UNDER_TESTING:
|
|
373
|
-
_field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
374
|
-
|
|
375
|
-
builder(
|
|
376
|
-
Field,
|
|
377
|
-
gatherer=lambda cls_: (_field_internal, {}),
|
|
378
|
-
methods=_field_methods,
|
|
379
|
-
flags={"slotted": True, "kw_only": True},
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
# Slot gathering tools
|
|
384
|
-
# Subclass of dict to be identifiable by isinstance checks
|
|
385
|
-
# For anything more complicated this could be made into a Mapping
|
|
386
|
-
class SlotFields(dict):
|
|
387
|
-
"""
|
|
388
|
-
A plain dict subclass.
|
|
389
|
-
|
|
390
|
-
For declaring slotfields there are no additional features required
|
|
391
|
-
other than recognising that this is intended to be used as a class
|
|
392
|
-
generating dict and isn't a regular dictionary that ended up in
|
|
393
|
-
`__slots__`.
|
|
394
|
-
|
|
395
|
-
This should be replaced on `__slots__` after fields have been gathered.
|
|
396
|
-
"""
|
|
397
|
-
|
|
398
|
-
|
|
399
631
|
def make_slot_gatherer(field_type=Field):
|
|
400
632
|
"""
|
|
401
633
|
Create a new annotation gatherer that will work with `Field` instances
|
|
@@ -405,14 +637,25 @@ def make_slot_gatherer(field_type=Field):
|
|
|
405
637
|
:return: A slot gatherer that will check for and generate Fields of
|
|
406
638
|
the type field_type.
|
|
407
639
|
"""
|
|
408
|
-
def field_slot_gatherer(
|
|
640
|
+
def field_slot_gatherer(cls_or_ns):
|
|
409
641
|
"""
|
|
410
642
|
Gather field information for class generation based on __slots__
|
|
411
643
|
|
|
412
|
-
:param
|
|
644
|
+
:param cls_or_ns: Class to gather field information from (or class namespace)
|
|
413
645
|
:return: dict of field_name: Field(...)
|
|
414
646
|
"""
|
|
415
|
-
|
|
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
|
+
)
|
|
416
659
|
|
|
417
660
|
if not isinstance(cls_slots, SlotFields):
|
|
418
661
|
raise TypeError(
|
|
@@ -422,14 +665,19 @@ def make_slot_gatherer(field_type=Field):
|
|
|
422
665
|
|
|
423
666
|
# Don't want to mutate original annotations so make a copy if it exists
|
|
424
667
|
# Looking at the dict is a Python3.9 or earlier requirement
|
|
425
|
-
cls_annotations =
|
|
426
|
-
**cls.__dict__.get("__annotations__", {})
|
|
427
|
-
}
|
|
668
|
+
cls_annotations = get_ns_annotations(cls_dict)
|
|
428
669
|
|
|
429
670
|
cls_fields = {}
|
|
430
671
|
slot_replacement = {}
|
|
431
672
|
|
|
432
673
|
for k, v in cls_slots.items():
|
|
674
|
+
# Special case __dict__ and __weakref__
|
|
675
|
+
# They should be included in the final `__slots__`
|
|
676
|
+
# But ignored as a value.
|
|
677
|
+
if k in {"__dict__", "__weakref__"}:
|
|
678
|
+
slot_replacement[k] = None
|
|
679
|
+
continue
|
|
680
|
+
|
|
433
681
|
if isinstance(v, field_type):
|
|
434
682
|
attrib = v
|
|
435
683
|
if attrib.type is not NOTHING:
|
|
@@ -454,43 +702,10 @@ def make_slot_gatherer(field_type=Field):
|
|
|
454
702
|
return field_slot_gatherer
|
|
455
703
|
|
|
456
704
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
def is_classvar(hint):
|
|
462
|
-
_typing = sys.modules.get("typing")
|
|
463
|
-
|
|
464
|
-
if _typing:
|
|
465
|
-
# Annotated is a nightmare I'm never waking up from
|
|
466
|
-
# 3.8 and 3.9 need Annotated from typing_extensions
|
|
467
|
-
# 3.8 also needs get_origin from typing_extensions
|
|
468
|
-
if sys.version_info < (3, 10):
|
|
469
|
-
_typing_extensions = sys.modules.get("typing_extensions")
|
|
470
|
-
if _typing_extensions:
|
|
471
|
-
_Annotated = _typing_extensions.Annotated
|
|
472
|
-
_get_origin = _typing_extensions.get_origin
|
|
473
|
-
else:
|
|
474
|
-
_Annotated, _get_origin = None, None
|
|
475
|
-
else:
|
|
476
|
-
_Annotated = _typing.Annotated
|
|
477
|
-
_get_origin = _typing.get_origin
|
|
478
|
-
|
|
479
|
-
if _Annotated and _get_origin(hint) is _Annotated:
|
|
480
|
-
hint = getattr(hint, "__origin__", None)
|
|
481
|
-
|
|
482
|
-
if (
|
|
483
|
-
hint is _typing.ClassVar
|
|
484
|
-
or getattr(hint, "__origin__", None) is _typing.ClassVar
|
|
485
|
-
):
|
|
486
|
-
return True
|
|
487
|
-
# String used as annotation
|
|
488
|
-
elif isinstance(hint, str) and "ClassVar" in hint:
|
|
489
|
-
return True
|
|
490
|
-
return False
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
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
|
+
):
|
|
494
709
|
"""
|
|
495
710
|
Create a new annotation gatherer that will work with `Field` instances
|
|
496
711
|
of the creators definition.
|
|
@@ -500,35 +715,51 @@ def make_annotation_gatherer(field_type=Field, leave_default_values=True):
|
|
|
500
715
|
default values in place as class variables.
|
|
501
716
|
:return: An annotation gatherer with these settings.
|
|
502
717
|
"""
|
|
503
|
-
def field_annotation_gatherer(
|
|
504
|
-
|
|
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__
|
|
505
723
|
|
|
506
724
|
cls_fields: dict[str, field_type] = {}
|
|
507
|
-
|
|
508
725
|
modifications = {}
|
|
509
726
|
|
|
727
|
+
cls_annotations = get_ns_annotations(cls_dict)
|
|
728
|
+
|
|
729
|
+
kw_flag = False
|
|
730
|
+
|
|
510
731
|
for k, v in cls_annotations.items():
|
|
511
732
|
# Ignore ClassVar
|
|
512
733
|
if is_classvar(v):
|
|
513
734
|
continue
|
|
514
735
|
|
|
515
|
-
|
|
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)
|
|
516
743
|
|
|
517
744
|
if attrib is not NOTHING:
|
|
518
745
|
if isinstance(attrib, field_type):
|
|
519
|
-
|
|
746
|
+
kw_only = attrib.kw_only or kw_flag
|
|
747
|
+
|
|
748
|
+
attrib = field_type.from_field(attrib, type=v, kw_only=kw_only)
|
|
749
|
+
|
|
520
750
|
if attrib.default is not NOTHING and leave_default_values:
|
|
521
751
|
modifications[k] = attrib.default
|
|
522
752
|
else:
|
|
523
753
|
# NOTHING sentinel indicates a value should be removed
|
|
524
754
|
modifications[k] = NOTHING
|
|
525
|
-
|
|
526
|
-
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)
|
|
527
757
|
if not leave_default_values:
|
|
528
758
|
modifications[k] = NOTHING
|
|
529
|
-
|
|
759
|
+
else:
|
|
760
|
+
attrib = field_type(type=v, kw_only=kw_flag)
|
|
530
761
|
else:
|
|
531
|
-
attrib = field_type(type=v)
|
|
762
|
+
attrib = field_type(type=v, kw_only=kw_flag)
|
|
532
763
|
|
|
533
764
|
cls_fields[k] = attrib
|
|
534
765
|
|
|
@@ -537,8 +768,106 @@ def make_annotation_gatherer(field_type=Field, leave_default_values=True):
|
|
|
537
768
|
return field_annotation_gatherer
|
|
538
769
|
|
|
539
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()
|
|
540
852
|
annotation_gatherer = make_annotation_gatherer()
|
|
541
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
|
+
|
|
542
871
|
|
|
543
872
|
def check_argument_order(cls):
|
|
544
873
|
"""
|
|
@@ -550,6 +879,9 @@ def check_argument_order(cls):
|
|
|
550
879
|
fields = get_fields(cls)
|
|
551
880
|
used_default = False
|
|
552
881
|
for k, v in fields.items():
|
|
882
|
+
if v.kw_only or (not v.init):
|
|
883
|
+
continue
|
|
884
|
+
|
|
553
885
|
if v.default is NOTHING and v.default_factory is NOTHING:
|
|
554
886
|
if used_default:
|
|
555
887
|
raise SyntaxError(
|
|
@@ -581,51 +913,33 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
|
|
|
581
913
|
return cls
|
|
582
914
|
|
|
583
915
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
return lambda cls_: annotationclass(cls_, methods=methods)
|
|
587
|
-
|
|
588
|
-
cls = builder(cls, gatherer=annotation_gatherer, methods=methods, flags={"slotted": False})
|
|
589
|
-
|
|
590
|
-
check_argument_order(cls)
|
|
591
|
-
|
|
592
|
-
return cls
|
|
916
|
+
class AnnotationClass(metaclass=SlotMakerMeta):
|
|
917
|
+
__slots__ = {}
|
|
593
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__
|
|
594
928
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
null=_NothingType(),
|
|
599
|
-
extra_code=["self.validate_field()"],
|
|
600
|
-
)
|
|
601
|
-
)
|
|
929
|
+
builder(cls, gatherer=gatherer, methods=methods, flags={"slotted": slots})
|
|
930
|
+
check_argument_order(cls)
|
|
931
|
+
super().__init_subclass__(**kwargs)
|
|
602
932
|
|
|
603
933
|
|
|
604
|
-
|
|
934
|
+
@slotclass
|
|
935
|
+
class GatheredFields:
|
|
605
936
|
"""
|
|
606
|
-
|
|
607
|
-
This works by forcing the __init__ method to treat NOTHING as a regular
|
|
608
|
-
value. This means *all* instance attributes always have defaults.
|
|
609
|
-
|
|
610
|
-
:param cls: Field subclass
|
|
611
|
-
:param frozen: Make the field class a frozen class.
|
|
612
|
-
Field classes are always frozen when running under `pytest`
|
|
613
|
-
:return: Modified subclass
|
|
937
|
+
A helper gatherer for fields that have been gathered externally.
|
|
614
938
|
"""
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
field_methods = {_field_init_desc, repr_maker, eq_maker}
|
|
619
|
-
|
|
620
|
-
# Always freeze when running tests
|
|
621
|
-
if frozen or _UNDER_TESTING:
|
|
622
|
-
field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
623
|
-
|
|
624
|
-
cls = builder(
|
|
625
|
-
cls,
|
|
626
|
-
gatherer=slot_gatherer,
|
|
627
|
-
methods=field_methods,
|
|
628
|
-
flags={"slotted": True, "kw_only": True}
|
|
939
|
+
__slots__ = SlotFields(
|
|
940
|
+
fields=Field(),
|
|
941
|
+
modifications=Field(),
|
|
629
942
|
)
|
|
630
943
|
|
|
631
|
-
|
|
944
|
+
def __call__(self, cls):
|
|
945
|
+
return self.fields, self.modifications
|