ducktools-classbuilder 0.3.0__py3-none-any.whl → 0.5.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 +298 -94
- ducktools/classbuilder/__init__.pyi +54 -13
- ducktools/classbuilder/prefab.py +48 -111
- ducktools/classbuilder/prefab.pyi +10 -18
- {ducktools_classbuilder-0.3.0.dist-info → ducktools_classbuilder-0.5.0.dist-info}/METADATA +38 -13
- ducktools_classbuilder-0.5.0.dist-info/RECORD +10 -0
- ducktools_classbuilder-0.3.0.dist-info/RECORD +0 -10
- {ducktools_classbuilder-0.3.0.dist-info → ducktools_classbuilder-0.5.0.dist-info}/LICENSE.md +0 -0
- {ducktools_classbuilder-0.3.0.dist-info → ducktools_classbuilder-0.5.0.dist-info}/WHEEL +0 -0
- {ducktools_classbuilder-0.3.0.dist-info → ducktools_classbuilder-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -19,12 +19,19 @@
|
|
|
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
|
-
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
__version__ = "v0.5.0"
|
|
23
25
|
|
|
24
26
|
# Change this name if you make heavy modifications
|
|
25
27
|
INTERNALS_DICT = "__classbuilder_internals__"
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
# If testing, make Field classes frozen to make sure attributes are not
|
|
31
|
+
# overwritten. When running this is a performance penalty so it is not required.
|
|
32
|
+
_UNDER_TESTING = "pytest" in sys.modules
|
|
33
|
+
|
|
34
|
+
|
|
28
35
|
def get_fields(cls, *, local=False):
|
|
29
36
|
"""
|
|
30
37
|
Utility function to gather the fields dictionary
|
|
@@ -58,7 +65,7 @@ def _get_inst_fields(inst):
|
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
|
|
61
|
-
# As 'None' can be a meaningful
|
|
68
|
+
# As 'None' can be a meaningful value we need a sentinel value
|
|
62
69
|
# to use to show no value has been provided.
|
|
63
70
|
class _NothingType:
|
|
64
71
|
def __repr__(self):
|
|
@@ -102,49 +109,55 @@ class MethodMaker:
|
|
|
102
109
|
return method.__get__(instance, cls)
|
|
103
110
|
|
|
104
111
|
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
def get_init_generator(null=NOTHING, extra_code=None):
|
|
113
|
+
def cls_init_maker(cls):
|
|
114
|
+
fields = get_fields(cls)
|
|
115
|
+
flags = get_flags(cls)
|
|
108
116
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
arglist = []
|
|
118
|
+
assignments = []
|
|
119
|
+
globs = {}
|
|
112
120
|
|
|
113
|
-
|
|
114
|
-
|
|
121
|
+
if flags.get("kw_only", False):
|
|
122
|
+
arglist.append("*")
|
|
115
123
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
for k, v in fields.items():
|
|
125
|
+
if v.default is not null:
|
|
126
|
+
globs[f"_{k}_default"] = v.default
|
|
127
|
+
arg = f"{k}=_{k}_default"
|
|
128
|
+
assignment = f"self.{k} = {k}"
|
|
129
|
+
elif v.default_factory is not null:
|
|
130
|
+
globs[f"_{k}_factory"] = v.default_factory
|
|
131
|
+
arg = f"{k}=None"
|
|
132
|
+
assignment = f"self.{k} = _{k}_factory() if {k} is None else {k}"
|
|
133
|
+
else:
|
|
134
|
+
arg = f"{k}"
|
|
135
|
+
assignment = f"self.{k} = {k}"
|
|
128
136
|
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
arglist.append(arg)
|
|
138
|
+
assignments.append(assignment)
|
|
131
139
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
args = ", ".join(arglist)
|
|
141
|
+
assigns = "\n ".join(assignments) if assignments else "pass\n"
|
|
142
|
+
code = (
|
|
143
|
+
f"def __init__(self, {args}):\n"
|
|
144
|
+
f" {assigns}\n"
|
|
145
|
+
)
|
|
146
|
+
# Handle additional function calls
|
|
147
|
+
# Used for validate_field on fieldclasses
|
|
148
|
+
if extra_code:
|
|
149
|
+
for line in extra_code:
|
|
150
|
+
code += f" {line}\n"
|
|
143
151
|
|
|
144
|
-
|
|
152
|
+
return code, globs
|
|
153
|
+
|
|
154
|
+
return cls_init_maker
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
init_generator = get_init_generator()
|
|
145
158
|
|
|
146
159
|
|
|
147
|
-
def
|
|
160
|
+
def repr_generator(cls):
|
|
148
161
|
fields = get_fields(cls)
|
|
149
162
|
content = ", ".join(
|
|
150
163
|
f"{name}={{self.{name}!r}}"
|
|
@@ -158,7 +171,7 @@ def repr_maker(cls):
|
|
|
158
171
|
return code, globs
|
|
159
172
|
|
|
160
173
|
|
|
161
|
-
def
|
|
174
|
+
def eq_generator(cls):
|
|
162
175
|
class_comparison = "self.__class__ is other.__class__"
|
|
163
176
|
field_names = get_fields(cls)
|
|
164
177
|
|
|
@@ -178,12 +191,55 @@ def eq_maker(cls):
|
|
|
178
191
|
return code, globs
|
|
179
192
|
|
|
180
193
|
|
|
194
|
+
def frozen_setattr_generator(cls):
|
|
195
|
+
globs = {}
|
|
196
|
+
field_names = set(get_fields(cls))
|
|
197
|
+
flags = get_flags(cls)
|
|
198
|
+
|
|
199
|
+
globs["__field_names"] = field_names
|
|
200
|
+
|
|
201
|
+
# Better to be safe and use the method that works in both cases
|
|
202
|
+
# if somehow slotted has not been set.
|
|
203
|
+
if flags.get("slotted", True):
|
|
204
|
+
globs["__setattr_func"] = object.__setattr__
|
|
205
|
+
setattr_method = "__setattr_func(self, name, value)"
|
|
206
|
+
else:
|
|
207
|
+
setattr_method = "self.__dict__[name] = value"
|
|
208
|
+
|
|
209
|
+
body = (
|
|
210
|
+
f" if hasattr(self, name) or name not in __field_names:\n"
|
|
211
|
+
f' raise TypeError(\n'
|
|
212
|
+
f' f"{{type(self).__name__!r}} object does not support "'
|
|
213
|
+
f' f"attribute assignment"\n'
|
|
214
|
+
f' )\n'
|
|
215
|
+
f" else:\n"
|
|
216
|
+
f" {setattr_method}\n"
|
|
217
|
+
)
|
|
218
|
+
code = f"def __setattr__(self, name, value):\n{body}"
|
|
219
|
+
|
|
220
|
+
return code, globs
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def frozen_delattr_generator(cls):
|
|
224
|
+
body = (
|
|
225
|
+
' raise TypeError(\n'
|
|
226
|
+
' f"{type(self).__name__!r} object "\n'
|
|
227
|
+
' f"does not support attribute deletion"\n'
|
|
228
|
+
' )\n'
|
|
229
|
+
)
|
|
230
|
+
code = f"def __delattr__(self, name):\n{body}"
|
|
231
|
+
globs = {}
|
|
232
|
+
return code, globs
|
|
233
|
+
|
|
234
|
+
|
|
181
235
|
# As only the __get__ method refers to the class we can use the same
|
|
182
236
|
# Descriptor instances for every class.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
237
|
+
init_maker = MethodMaker("__init__", init_generator)
|
|
238
|
+
repr_maker = MethodMaker("__repr__", repr_generator)
|
|
239
|
+
eq_maker = MethodMaker("__eq__", eq_generator)
|
|
240
|
+
frozen_setattr_maker = MethodMaker("__setattr__", frozen_setattr_generator)
|
|
241
|
+
frozen_delattr_maker = MethodMaker("__delattr__", frozen_delattr_generator)
|
|
242
|
+
default_methods = frozenset({init_maker, repr_maker, eq_maker})
|
|
187
243
|
|
|
188
244
|
|
|
189
245
|
def builder(cls=None, /, *, gatherer, methods, flags=None):
|
|
@@ -192,7 +248,7 @@ def builder(cls=None, /, *, gatherer, methods, flags=None):
|
|
|
192
248
|
|
|
193
249
|
:param cls: Class to be analysed and have methods generated
|
|
194
250
|
:param gatherer: Function to gather field information
|
|
195
|
-
:type gatherer: Callable[[type], dict[str, Field]]
|
|
251
|
+
:type gatherer: Callable[[type], tuple[dict[str, Field], dict[str, Any]]]
|
|
196
252
|
:param methods: MethodMakers to add to the class
|
|
197
253
|
:type methods: set[MethodMaker]
|
|
198
254
|
:param flags: additional flags to store in the internals dictionary
|
|
@@ -211,7 +267,14 @@ def builder(cls=None, /, *, gatherer, methods, flags=None):
|
|
|
211
267
|
internals = {}
|
|
212
268
|
setattr(cls, INTERNALS_DICT, internals)
|
|
213
269
|
|
|
214
|
-
cls_fields = gatherer(cls)
|
|
270
|
+
cls_fields, modifications = gatherer(cls)
|
|
271
|
+
|
|
272
|
+
for name, value in modifications.items():
|
|
273
|
+
if value is NOTHING:
|
|
274
|
+
delattr(cls, name)
|
|
275
|
+
else:
|
|
276
|
+
setattr(cls, name, value)
|
|
277
|
+
|
|
215
278
|
internals["local_fields"] = cls_fields
|
|
216
279
|
|
|
217
280
|
mro = cls.__mro__[:-1] # skip 'object' base class
|
|
@@ -244,6 +307,8 @@ class Field:
|
|
|
244
307
|
some metadata.
|
|
245
308
|
|
|
246
309
|
Intended to be extendable by subclasses for additional features.
|
|
310
|
+
|
|
311
|
+
Note: When run under `pytest`, Field instances are Frozen.
|
|
247
312
|
"""
|
|
248
313
|
__slots__ = {
|
|
249
314
|
"default": "Standard default value to be used for attributes with"
|
|
@@ -303,14 +368,19 @@ _field_internal = {
|
|
|
303
368
|
"doc": Field(default=None),
|
|
304
369
|
}
|
|
305
370
|
|
|
371
|
+
_field_methods = {repr_maker, eq_maker}
|
|
372
|
+
if _UNDER_TESTING:
|
|
373
|
+
_field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
374
|
+
|
|
306
375
|
builder(
|
|
307
376
|
Field,
|
|
308
|
-
gatherer=lambda cls_: _field_internal,
|
|
309
|
-
methods=
|
|
377
|
+
gatherer=lambda cls_: (_field_internal, {}),
|
|
378
|
+
methods=_field_methods,
|
|
310
379
|
flags={"slotted": True, "kw_only": True},
|
|
311
380
|
)
|
|
312
381
|
|
|
313
382
|
|
|
383
|
+
# Slot gathering tools
|
|
314
384
|
# Subclass of dict to be identifiable by isinstance checks
|
|
315
385
|
# For anything more complicated this could be made into a Mapping
|
|
316
386
|
class SlotFields(dict):
|
|
@@ -326,46 +396,170 @@ class SlotFields(dict):
|
|
|
326
396
|
"""
|
|
327
397
|
|
|
328
398
|
|
|
329
|
-
def
|
|
399
|
+
def make_slot_gatherer(field_type=Field):
|
|
330
400
|
"""
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
:
|
|
401
|
+
Create a new annotation gatherer that will work with `Field` instances
|
|
402
|
+
of the creators definition.
|
|
403
|
+
|
|
404
|
+
:param field_type: The `Field` classes to be used when gathering fields
|
|
405
|
+
:return: A slot gatherer that will check for and generate Fields of
|
|
406
|
+
the type field_type.
|
|
335
407
|
"""
|
|
336
|
-
|
|
408
|
+
def field_slot_gatherer(cls):
|
|
409
|
+
"""
|
|
410
|
+
Gather field information for class generation based on __slots__
|
|
411
|
+
|
|
412
|
+
:param cls: Class to gather field information from
|
|
413
|
+
:return: dict of field_name: Field(...)
|
|
414
|
+
"""
|
|
415
|
+
cls_slots = cls.__dict__.get("__slots__", None)
|
|
416
|
+
|
|
417
|
+
if not isinstance(cls_slots, SlotFields):
|
|
418
|
+
raise TypeError(
|
|
419
|
+
"__slots__ must be an instance of SlotFields "
|
|
420
|
+
"in order to generate a slotclass"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# Don't want to mutate original annotations so make a copy if it exists
|
|
424
|
+
# Looking at the dict is a Python3.9 or earlier requirement
|
|
425
|
+
cls_annotations = {
|
|
426
|
+
**cls.__dict__.get("__annotations__", {})
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
cls_fields = {}
|
|
430
|
+
slot_replacement = {}
|
|
431
|
+
|
|
432
|
+
for k, v in cls_slots.items():
|
|
433
|
+
if isinstance(v, field_type):
|
|
434
|
+
attrib = v
|
|
435
|
+
if attrib.type is not NOTHING:
|
|
436
|
+
cls_annotations[k] = attrib.type
|
|
437
|
+
else:
|
|
438
|
+
# Plain values treated as defaults
|
|
439
|
+
attrib = field_type(default=v)
|
|
440
|
+
|
|
441
|
+
slot_replacement[k] = attrib.doc
|
|
442
|
+
cls_fields[k] = attrib
|
|
443
|
+
|
|
444
|
+
# Send the modifications to the builder for what should be changed
|
|
445
|
+
# On the class.
|
|
446
|
+
# In this case, slots with documentation and new annotations.
|
|
447
|
+
modifications = {
|
|
448
|
+
"__slots__": slot_replacement,
|
|
449
|
+
"__annotations__": cls_annotations,
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return cls_fields, modifications
|
|
453
|
+
|
|
454
|
+
return field_slot_gatherer
|
|
337
455
|
|
|
338
|
-
if not isinstance(cls_slots, SlotFields):
|
|
339
|
-
raise TypeError(
|
|
340
|
-
"__slots__ must be an instance of SlotFields "
|
|
341
|
-
"in order to generate a slotclass"
|
|
342
|
-
)
|
|
343
456
|
|
|
344
|
-
|
|
345
|
-
cls_fields = {}
|
|
346
|
-
slot_replacement = {}
|
|
457
|
+
slot_gatherer = make_slot_gatherer()
|
|
347
458
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
459
|
+
|
|
460
|
+
# Annotation gathering tools
|
|
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
|
|
353
475
|
else:
|
|
354
|
-
|
|
355
|
-
|
|
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):
|
|
494
|
+
"""
|
|
495
|
+
Create a new annotation gatherer that will work with `Field` instances
|
|
496
|
+
of the creators definition.
|
|
497
|
+
|
|
498
|
+
:param field_type: The `Field` classes to be used when gathering fields
|
|
499
|
+
:param leave_default_values: Set to True if the gatherer should leave
|
|
500
|
+
default values in place as class variables.
|
|
501
|
+
:return: An annotation gatherer with these settings.
|
|
502
|
+
"""
|
|
503
|
+
def field_annotation_gatherer(cls):
|
|
504
|
+
cls_annotations = cls.__dict__.get("__annotations__", {})
|
|
505
|
+
|
|
506
|
+
cls_fields: dict[str, field_type] = {}
|
|
507
|
+
|
|
508
|
+
modifications = {}
|
|
509
|
+
|
|
510
|
+
for k, v in cls_annotations.items():
|
|
511
|
+
# Ignore ClassVar
|
|
512
|
+
if is_classvar(v):
|
|
513
|
+
continue
|
|
514
|
+
|
|
515
|
+
attrib = getattr(cls, k, NOTHING)
|
|
516
|
+
|
|
517
|
+
if attrib is not NOTHING:
|
|
518
|
+
if isinstance(attrib, field_type):
|
|
519
|
+
attrib = field_type.from_field(attrib, type=v)
|
|
520
|
+
if attrib.default is not NOTHING and leave_default_values:
|
|
521
|
+
modifications[k] = attrib.default
|
|
522
|
+
else:
|
|
523
|
+
# NOTHING sentinel indicates a value should be removed
|
|
524
|
+
modifications[k] = NOTHING
|
|
525
|
+
else:
|
|
526
|
+
attrib = field_type(default=attrib, type=v)
|
|
527
|
+
if not leave_default_values:
|
|
528
|
+
modifications[k] = NOTHING
|
|
529
|
+
|
|
530
|
+
else:
|
|
531
|
+
attrib = field_type(type=v)
|
|
532
|
+
|
|
533
|
+
cls_fields[k] = attrib
|
|
356
534
|
|
|
357
|
-
|
|
358
|
-
cls_fields[k] = attrib
|
|
535
|
+
return cls_fields, modifications
|
|
359
536
|
|
|
360
|
-
|
|
361
|
-
# So that help() works
|
|
362
|
-
setattr(cls, "__slots__", slot_replacement)
|
|
537
|
+
return field_annotation_gatherer
|
|
363
538
|
|
|
364
|
-
# Update annotations with any types from the slots assignment
|
|
365
|
-
setattr(cls, "__annotations__", cls_annotations)
|
|
366
|
-
return cls_fields
|
|
367
539
|
|
|
540
|
+
annotation_gatherer = make_annotation_gatherer()
|
|
368
541
|
|
|
542
|
+
|
|
543
|
+
def check_argument_order(cls):
|
|
544
|
+
"""
|
|
545
|
+
Raise a SyntaxError if the argument order will be invalid for a generated
|
|
546
|
+
`__init__` function.
|
|
547
|
+
|
|
548
|
+
:param cls: class being built
|
|
549
|
+
"""
|
|
550
|
+
fields = get_fields(cls)
|
|
551
|
+
used_default = False
|
|
552
|
+
for k, v in fields.items():
|
|
553
|
+
if v.default is NOTHING and v.default_factory is NOTHING:
|
|
554
|
+
if used_default:
|
|
555
|
+
raise SyntaxError(
|
|
556
|
+
f"non-default argument {k!r} follows default argument"
|
|
557
|
+
)
|
|
558
|
+
else:
|
|
559
|
+
used_default = True
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# Class Decorators
|
|
369
563
|
def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
|
|
370
564
|
"""
|
|
371
565
|
Example of class builder in action using __slots__ to find fields.
|
|
@@ -382,40 +576,50 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
|
|
|
382
576
|
cls = builder(cls, gatherer=slot_gatherer, methods=methods, flags={"slotted": True})
|
|
383
577
|
|
|
384
578
|
if syntax_check:
|
|
385
|
-
|
|
386
|
-
used_default = False
|
|
387
|
-
for k, v in fields.items():
|
|
388
|
-
if v.default is NOTHING and v.default_factory is NOTHING:
|
|
389
|
-
if used_default:
|
|
390
|
-
raise SyntaxError(
|
|
391
|
-
f"non-default argument {k!r} follows default argument"
|
|
392
|
-
)
|
|
393
|
-
else:
|
|
394
|
-
used_default = True
|
|
579
|
+
check_argument_order(cls)
|
|
395
580
|
|
|
396
581
|
return cls
|
|
397
582
|
|
|
398
583
|
|
|
399
|
-
def
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
584
|
+
def annotationclass(cls=None, /, *, methods=default_methods):
|
|
585
|
+
if not cls:
|
|
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
|
|
405
593
|
|
|
406
594
|
|
|
407
|
-
|
|
595
|
+
_field_init_desc = MethodMaker(
|
|
596
|
+
funcname="__init__",
|
|
597
|
+
code_generator=get_init_generator(
|
|
598
|
+
null=_NothingType(),
|
|
599
|
+
extra_code=["self.validate_field()"],
|
|
600
|
+
)
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def fieldclass(cls=None, /, *, frozen=False):
|
|
408
605
|
"""
|
|
409
606
|
This is a special decorator for making Field subclasses using __slots__.
|
|
410
607
|
This works by forcing the __init__ method to treat NOTHING as a regular
|
|
411
608
|
value. This means *all* instance attributes always have defaults.
|
|
412
609
|
|
|
413
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`
|
|
414
613
|
:return: Modified subclass
|
|
415
614
|
"""
|
|
615
|
+
if not cls:
|
|
616
|
+
return lambda cls_: fieldclass(cls_, frozen=frozen)
|
|
617
|
+
|
|
618
|
+
field_methods = {_field_init_desc, repr_maker, eq_maker}
|
|
416
619
|
|
|
417
|
-
|
|
418
|
-
|
|
620
|
+
# Always freeze when running tests
|
|
621
|
+
if frozen or _UNDER_TESTING:
|
|
622
|
+
field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
419
623
|
|
|
420
624
|
cls = builder(
|
|
421
625
|
cls,
|
|
@@ -26,18 +26,24 @@ class MethodMaker:
|
|
|
26
26
|
def __repr__(self) -> str: ...
|
|
27
27
|
def __get__(self, instance, cls) -> Callable: ...
|
|
28
28
|
|
|
29
|
-
def
|
|
30
|
-
cls: type,
|
|
31
|
-
*,
|
|
29
|
+
def get_init_generator(
|
|
32
30
|
null: _NothingType = NOTHING,
|
|
33
31
|
extra_code: None | list[str] = None
|
|
34
|
-
) -> tuple[str, dict[str, typing.Any]]: ...
|
|
35
|
-
|
|
36
|
-
def
|
|
32
|
+
) -> Callable[[type], tuple[str, dict[str, typing.Any]]]: ...
|
|
33
|
+
|
|
34
|
+
def init_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
|
|
35
|
+
def repr_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
|
|
36
|
+
def eq_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
|
|
37
|
+
|
|
38
|
+
def frozen_setattr_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
def frozen_delattr_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
|
|
41
|
+
|
|
42
|
+
init_maker: MethodMaker
|
|
43
|
+
repr_maker: MethodMaker
|
|
44
|
+
eq_maker: MethodMaker
|
|
45
|
+
frozen_setattr_maker: MethodMaker
|
|
46
|
+
frozen_delattr_maker: MethodMaker
|
|
41
47
|
default_methods: frozenset[MethodMaker]
|
|
42
48
|
|
|
43
49
|
_T = typing.TypeVar("_T")
|
|
@@ -47,7 +53,7 @@ def builder(
|
|
|
47
53
|
cls: type[_T],
|
|
48
54
|
/,
|
|
49
55
|
*,
|
|
50
|
-
gatherer: Callable[[type], dict[str, Field]],
|
|
56
|
+
gatherer: Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]],
|
|
51
57
|
methods: frozenset[MethodMaker] | set[MethodMaker],
|
|
52
58
|
flags: dict[str, bool] | None = None,
|
|
53
59
|
) -> type[_T]: ...
|
|
@@ -57,7 +63,7 @@ def builder(
|
|
|
57
63
|
cls: None = None,
|
|
58
64
|
/,
|
|
59
65
|
*,
|
|
60
|
-
gatherer: Callable[[type], dict[str, Field]],
|
|
66
|
+
gatherer: Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]],
|
|
61
67
|
methods: frozenset[MethodMaker] | set[MethodMaker],
|
|
62
68
|
flags: dict[str, bool] | None = None,
|
|
63
69
|
) -> Callable[[type[_T]], type[_T]]: ...
|
|
@@ -89,9 +95,24 @@ class Field:
|
|
|
89
95
|
class SlotFields(dict):
|
|
90
96
|
...
|
|
91
97
|
|
|
92
|
-
def
|
|
98
|
+
def make_slot_gatherer(
|
|
99
|
+
field_type: type[Field] = Field
|
|
100
|
+
) -> Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
|
|
101
|
+
|
|
102
|
+
def slot_gatherer(cls: type) -> tuple[dict[str, Field], dict[str, typing.Any]]:
|
|
93
103
|
...
|
|
94
104
|
|
|
105
|
+
def is_classvar(hint: object) -> bool: ...
|
|
106
|
+
|
|
107
|
+
def make_annotation_gatherer(
|
|
108
|
+
field_type: type[Field] = Field,
|
|
109
|
+
leave_default_values: bool = True,
|
|
110
|
+
) -> Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
|
|
111
|
+
|
|
112
|
+
def annotation_gatherer(cls: type) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
|
|
113
|
+
|
|
114
|
+
def check_argument_order(cls: type) -> None: ...
|
|
115
|
+
|
|
95
116
|
@typing.overload
|
|
96
117
|
def slotclass(
|
|
97
118
|
cls: type[_T],
|
|
@@ -110,4 +131,24 @@ def slotclass(
|
|
|
110
131
|
syntax_check: bool = True
|
|
111
132
|
) -> Callable[[type[_T]], type[_T]]: ...
|
|
112
133
|
|
|
113
|
-
|
|
134
|
+
@typing.overload
|
|
135
|
+
def annotationclass(
|
|
136
|
+
cls: type[_T],
|
|
137
|
+
/,
|
|
138
|
+
*,
|
|
139
|
+
methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
|
|
140
|
+
) -> type[_T]: ...
|
|
141
|
+
|
|
142
|
+
@typing.overload
|
|
143
|
+
def annotationclass(
|
|
144
|
+
cls: None = None,
|
|
145
|
+
/,
|
|
146
|
+
*,
|
|
147
|
+
methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
|
|
148
|
+
) -> Callable[[type[_T]], type[_T]]: ...
|
|
149
|
+
|
|
150
|
+
@typing.overload
|
|
151
|
+
def fieldclass(cls: type[_T], /, *, frozen: bool = False) -> type[_T]: ...
|
|
152
|
+
|
|
153
|
+
@typing.overload
|
|
154
|
+
def fieldclass(cls: None = None, /, *, frozen: bool = False) -> Callable[[type[_T]], type[_T]]: ...
|
ducktools/classbuilder/prefab.py
CHANGED
|
@@ -31,7 +31,8 @@ import sys
|
|
|
31
31
|
from . import (
|
|
32
32
|
INTERNALS_DICT, NOTHING,
|
|
33
33
|
Field, MethodMaker, SlotFields,
|
|
34
|
-
builder, fieldclass, get_flags, get_fields,
|
|
34
|
+
builder, fieldclass, get_flags, get_fields, make_slot_gatherer,
|
|
35
|
+
frozen_setattr_maker, frozen_delattr_maker, is_classvar,
|
|
35
36
|
)
|
|
36
37
|
|
|
37
38
|
PREFAB_FIELDS = "PREFAB_FIELDS"
|
|
@@ -55,20 +56,6 @@ class PrefabError(Exception):
|
|
|
55
56
|
pass
|
|
56
57
|
|
|
57
58
|
|
|
58
|
-
def _is_classvar(hint):
|
|
59
|
-
_typing = sys.modules.get("typing")
|
|
60
|
-
if _typing:
|
|
61
|
-
if (
|
|
62
|
-
hint is _typing.ClassVar
|
|
63
|
-
or getattr(hint, "__origin__", None) is _typing.ClassVar
|
|
64
|
-
):
|
|
65
|
-
return True
|
|
66
|
-
# String used as annotation
|
|
67
|
-
elif isinstance(hint, str) and "ClassVar" in hint:
|
|
68
|
-
return True
|
|
69
|
-
return False
|
|
70
|
-
|
|
71
|
-
|
|
72
59
|
def get_attributes(cls):
|
|
73
60
|
"""
|
|
74
61
|
Copy of get_fields, typed to return Attribute instead of Field.
|
|
@@ -340,57 +327,6 @@ def get_iter_maker():
|
|
|
340
327
|
return MethodMaker("__iter__", __iter__)
|
|
341
328
|
|
|
342
329
|
|
|
343
|
-
def get_frozen_setattr_maker():
|
|
344
|
-
def __setattr__(cls: "type") -> "tuple[str, dict]":
|
|
345
|
-
globs = {}
|
|
346
|
-
attributes = get_attributes(cls)
|
|
347
|
-
flags = get_flags(cls)
|
|
348
|
-
|
|
349
|
-
# Make the fields set literal
|
|
350
|
-
fields_delimited = ", ".join(f"{field!r}" for field in attributes)
|
|
351
|
-
field_set = f"{{ {fields_delimited} }}"
|
|
352
|
-
|
|
353
|
-
# Better to be safe and use the method that works in both cases
|
|
354
|
-
# if somehow slotted has not been set.
|
|
355
|
-
if flags.get("slotted", True):
|
|
356
|
-
globs["__prefab_setattr_func"] = object.__setattr__
|
|
357
|
-
setattr_method = "__prefab_setattr_func(self, name, value)"
|
|
358
|
-
else:
|
|
359
|
-
setattr_method = "self.__dict__[name] = value"
|
|
360
|
-
|
|
361
|
-
body = (
|
|
362
|
-
f" if hasattr(self, name) or name not in {field_set}:\n"
|
|
363
|
-
f' raise TypeError(\n'
|
|
364
|
-
f' f"{{type(self).__name__!r}} object does not support "'
|
|
365
|
-
f' f"attribute assignment"\n'
|
|
366
|
-
f' )\n'
|
|
367
|
-
f" else:\n"
|
|
368
|
-
f" {setattr_method}\n"
|
|
369
|
-
)
|
|
370
|
-
code = f"def __setattr__(self, name, value):\n{body}"
|
|
371
|
-
|
|
372
|
-
return code, globs
|
|
373
|
-
|
|
374
|
-
# Pass the exception to exec
|
|
375
|
-
return MethodMaker("__setattr__", __setattr__)
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
def get_frozen_delattr_maker():
|
|
379
|
-
# noinspection PyUnusedLocal
|
|
380
|
-
def __delattr__(cls: "type") -> "tuple[str, dict]":
|
|
381
|
-
body = (
|
|
382
|
-
' raise TypeError(\n'
|
|
383
|
-
' f"{type(self).__name__!r} object "\n'
|
|
384
|
-
' f"does not support attribute deletion"\n'
|
|
385
|
-
' )\n'
|
|
386
|
-
)
|
|
387
|
-
code = f"def __delattr__(self, name):\n{body}"
|
|
388
|
-
globs = {}
|
|
389
|
-
return code, globs
|
|
390
|
-
|
|
391
|
-
return MethodMaker("__delattr__", __delattr__)
|
|
392
|
-
|
|
393
|
-
|
|
394
330
|
def get_asdict_maker():
|
|
395
331
|
def as_dict_gen(cls: "type") -> "tuple[str, dict]":
|
|
396
332
|
fields = get_attributes(cls)
|
|
@@ -408,15 +344,13 @@ def get_asdict_maker():
|
|
|
408
344
|
return MethodMaker("as_dict", as_dict_gen)
|
|
409
345
|
|
|
410
346
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
frozen_delattr_desc = get_frozen_delattr_maker()
|
|
419
|
-
asdict_desc = get_asdict_maker()
|
|
347
|
+
init_maker = get_init_maker()
|
|
348
|
+
prefab_init_maker = get_init_maker(init_name=PREFAB_INIT_FUNC)
|
|
349
|
+
repr_maker = get_repr_maker()
|
|
350
|
+
recursive_repr_maker = get_repr_maker(recursion_safe=True)
|
|
351
|
+
eq_maker = get_eq_maker()
|
|
352
|
+
iter_maker = get_iter_maker()
|
|
353
|
+
asdict_maker = get_asdict_maker()
|
|
420
354
|
|
|
421
355
|
|
|
422
356
|
# Updated field with additional attributes
|
|
@@ -491,12 +425,7 @@ def attribute(
|
|
|
491
425
|
)
|
|
492
426
|
|
|
493
427
|
|
|
494
|
-
|
|
495
|
-
# For prefabs it's easier if everything is an attribute
|
|
496
|
-
return {
|
|
497
|
-
name: Attribute.from_field(fld)
|
|
498
|
-
for name, fld in slot_gatherer(cls).items()
|
|
499
|
-
}
|
|
428
|
+
slot_prefab_gatherer = make_slot_gatherer(Attribute)
|
|
500
429
|
|
|
501
430
|
|
|
502
431
|
# Gatherer for classes built on attributes or annotations
|
|
@@ -512,6 +441,8 @@ def attribute_gatherer(cls):
|
|
|
512
441
|
|
|
513
442
|
cls_attribute_names = cls_attributes.keys()
|
|
514
443
|
|
|
444
|
+
cls_modifications = {}
|
|
445
|
+
|
|
515
446
|
if set(cls_annotation_names).issuperset(set(cls_attribute_names)):
|
|
516
447
|
# replace the classes' attributes dict with one with the correct
|
|
517
448
|
# order from the annotations.
|
|
@@ -519,7 +450,7 @@ def attribute_gatherer(cls):
|
|
|
519
450
|
new_attributes = {}
|
|
520
451
|
for name, value in cls_annotations.items():
|
|
521
452
|
# Ignore ClassVar hints
|
|
522
|
-
if
|
|
453
|
+
if is_classvar(value):
|
|
523
454
|
continue
|
|
524
455
|
|
|
525
456
|
# Look for the KW_ONLY annotation
|
|
@@ -533,39 +464,45 @@ def attribute_gatherer(cls):
|
|
|
533
464
|
# Copy attributes that are already defined to the new dict
|
|
534
465
|
# generate Attribute() values for those that are not defined.
|
|
535
466
|
|
|
467
|
+
# Extra parameters to pass to each Attribute
|
|
468
|
+
extras = {
|
|
469
|
+
"type": cls_annotations[name]
|
|
470
|
+
}
|
|
471
|
+
if kw_flag:
|
|
472
|
+
extras["kw_only"] = True
|
|
473
|
+
|
|
536
474
|
# If a field name is also declared in slots it can't have a real
|
|
537
475
|
# default value and the attr will be the slot descriptor.
|
|
538
476
|
if hasattr(cls, name) and name not in cls_slots:
|
|
539
477
|
if name in cls_attribute_names:
|
|
540
|
-
attrib =
|
|
478
|
+
attrib = Attribute.from_field(
|
|
479
|
+
cls_attributes[name],
|
|
480
|
+
**extras,
|
|
481
|
+
)
|
|
541
482
|
else:
|
|
542
483
|
attribute_default = getattr(cls, name)
|
|
543
|
-
attrib = attribute(default=attribute_default)
|
|
484
|
+
attrib = attribute(default=attribute_default, **extras)
|
|
544
485
|
|
|
545
486
|
# Clear the attribute from the class after it has been used
|
|
546
487
|
# in the definition.
|
|
547
|
-
|
|
488
|
+
cls_modifications[name] = NOTHING
|
|
548
489
|
else:
|
|
549
|
-
attrib = attribute()
|
|
490
|
+
attrib = attribute(**extras)
|
|
550
491
|
|
|
551
|
-
if kw_flag:
|
|
552
|
-
attrib.kw_only = True
|
|
553
|
-
|
|
554
|
-
attrib.type = cls_annotations[name]
|
|
555
492
|
new_attributes[name] = attrib
|
|
556
493
|
|
|
557
494
|
cls_attributes = new_attributes
|
|
558
495
|
else:
|
|
559
|
-
for name
|
|
560
|
-
|
|
496
|
+
for name in cls_attributes.keys():
|
|
497
|
+
attrib = cls_attributes[name]
|
|
498
|
+
cls_modifications[name] = NOTHING
|
|
561
499
|
|
|
562
500
|
# Some items can still be annotated.
|
|
563
|
-
|
|
564
|
-
attrib
|
|
565
|
-
|
|
566
|
-
pass
|
|
501
|
+
if name in cls_annotations:
|
|
502
|
+
new_attrib = Attribute.from_field(attrib, type=cls_annotations[name])
|
|
503
|
+
cls_attributes[name] = new_attrib
|
|
567
504
|
|
|
568
|
-
return cls_attributes
|
|
505
|
+
return cls_attributes, cls_modifications
|
|
569
506
|
|
|
570
507
|
|
|
571
508
|
# Class Builders
|
|
@@ -619,24 +556,24 @@ def _make_prefab(
|
|
|
619
556
|
methods = set()
|
|
620
557
|
|
|
621
558
|
if init and "__init__" not in cls_dict:
|
|
622
|
-
methods.add(
|
|
559
|
+
methods.add(init_maker)
|
|
623
560
|
else:
|
|
624
|
-
methods.add(
|
|
561
|
+
methods.add(prefab_init_maker)
|
|
625
562
|
|
|
626
563
|
if repr and "__repr__" not in cls_dict:
|
|
627
564
|
if recursive_repr:
|
|
628
|
-
methods.add(
|
|
565
|
+
methods.add(recursive_repr_maker)
|
|
629
566
|
else:
|
|
630
|
-
methods.add(
|
|
567
|
+
methods.add(repr_maker)
|
|
631
568
|
if eq and "__eq__" not in cls_dict:
|
|
632
|
-
methods.add(
|
|
569
|
+
methods.add(eq_maker)
|
|
633
570
|
if iter and "__iter__" not in cls_dict:
|
|
634
|
-
methods.add(
|
|
571
|
+
methods.add(iter_maker)
|
|
635
572
|
if frozen:
|
|
636
|
-
methods.add(
|
|
637
|
-
methods.add(
|
|
573
|
+
methods.add(frozen_setattr_maker)
|
|
574
|
+
methods.add(frozen_delattr_maker)
|
|
638
575
|
if dict_method:
|
|
639
|
-
methods.add(
|
|
576
|
+
methods.add(asdict_maker)
|
|
640
577
|
|
|
641
578
|
flags = {
|
|
642
579
|
"kw_only": kw_only,
|
|
@@ -912,17 +849,17 @@ def as_dict(o):
|
|
|
912
849
|
:param o: instance of a prefab class
|
|
913
850
|
:return: dictionary of {k: v} from fields
|
|
914
851
|
"""
|
|
852
|
+
cls = type(o)
|
|
853
|
+
if not hasattr(cls, PREFAB_FIELDS):
|
|
854
|
+
raise TypeError(f"{o!r} should be a prefab instance, not {cls}")
|
|
855
|
+
|
|
915
856
|
# Attempt to use the generated method if available
|
|
916
857
|
try:
|
|
917
858
|
return o.as_dict()
|
|
918
859
|
except AttributeError:
|
|
919
860
|
pass
|
|
920
861
|
|
|
921
|
-
|
|
922
|
-
try:
|
|
923
|
-
flds = get_attributes(cls)
|
|
924
|
-
except AttributeError:
|
|
925
|
-
raise TypeError(f"inst should be a prefab instance, not {cls}")
|
|
862
|
+
flds = get_attributes(cls)
|
|
926
863
|
|
|
927
864
|
return {
|
|
928
865
|
name: getattr(o, name)
|
|
@@ -6,7 +6,7 @@ from collections.abc import Callable
|
|
|
6
6
|
from . import (
|
|
7
7
|
INTERNALS_DICT, NOTHING,
|
|
8
8
|
Field, MethodMaker, SlotFields as SlotFields,
|
|
9
|
-
builder, fieldclass, get_flags, get_fields,
|
|
9
|
+
builder, fieldclass, get_flags, get_fields, make_slot_gatherer
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
# noinspection PyUnresolvedReferences
|
|
@@ -26,8 +26,6 @@ KW_ONLY: _KW_ONLY_TYPE
|
|
|
26
26
|
|
|
27
27
|
class PrefabError(Exception): ...
|
|
28
28
|
|
|
29
|
-
def _is_classvar(hint: type | str) -> bool: ...
|
|
30
|
-
|
|
31
29
|
def get_attributes(cls: type) -> dict[str, Attribute]: ...
|
|
32
30
|
|
|
33
31
|
def get_init_maker(*, init_name: str="__init__") -> MethodMaker: ...
|
|
@@ -38,22 +36,16 @@ def get_eq_maker() -> MethodMaker: ...
|
|
|
38
36
|
|
|
39
37
|
def get_iter_maker() -> MethodMaker: ...
|
|
40
38
|
|
|
41
|
-
def get_frozen_setattr_maker() -> MethodMaker: ...
|
|
42
|
-
|
|
43
|
-
def get_frozen_delattr_maker() -> MethodMaker: ...
|
|
44
|
-
|
|
45
39
|
def get_asdict_maker() -> MethodMaker: ...
|
|
46
40
|
|
|
47
41
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
frozen_delattr_desc: MethodMaker
|
|
56
|
-
asdict_desc: MethodMaker
|
|
42
|
+
init_maker: MethodMaker
|
|
43
|
+
prefab_init_maker: MethodMaker
|
|
44
|
+
repr_maker: MethodMaker
|
|
45
|
+
recursive_repr_maker: MethodMaker
|
|
46
|
+
eq_maker: MethodMaker
|
|
47
|
+
iter_maker: MethodMaker
|
|
48
|
+
asdict_maker: MethodMaker
|
|
57
49
|
|
|
58
50
|
class Attribute(Field):
|
|
59
51
|
__slots__: dict
|
|
@@ -101,9 +93,9 @@ def attribute(
|
|
|
101
93
|
exclude_field: bool = False,
|
|
102
94
|
) -> Attribute: ...
|
|
103
95
|
|
|
104
|
-
def slot_prefab_gatherer(cls: type) -> dict[str, Attribute]: ...
|
|
96
|
+
def slot_prefab_gatherer(cls: type) -> tuple[dict[str, Attribute], dict[str, typing.Any]]: ...
|
|
105
97
|
|
|
106
|
-
def attribute_gatherer(cls: type) -> dict[str, Attribute]: ...
|
|
98
|
+
def attribute_gatherer(cls: type) -> tuple[dict[str, Attribute], dict[str, typing.Any]]: ...
|
|
107
99
|
|
|
108
100
|
def _make_prefab(
|
|
109
101
|
cls: type,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ducktools-classbuilder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Toolkit for creating class boilerplate generators
|
|
5
5
|
Author: David C Ellis
|
|
6
6
|
License: MIT License
|
|
@@ -45,6 +45,7 @@ Provides-Extra: testing
|
|
|
45
45
|
Requires-Dist: pytest ; extra == 'testing'
|
|
46
46
|
Requires-Dist: pytest-cov ; extra == 'testing'
|
|
47
47
|
Requires-Dist: mypy ; extra == 'testing'
|
|
48
|
+
Requires-Dist: typing-extensions ; (python_version < "3.10") and extra == 'testing'
|
|
48
49
|
|
|
49
50
|
# Ducktools: Class Builder #
|
|
50
51
|
|
|
@@ -68,11 +69,12 @@ Install from PyPI with:
|
|
|
68
69
|
In order to create a class decorator using `ducktools.classbuilder` there are
|
|
69
70
|
a few things you need to prepare.
|
|
70
71
|
|
|
71
|
-
1. A field gathering function to analyse the class and collect valid `Field`s
|
|
72
|
+
1. A field gathering function to analyse the class and collect valid `Field`s and provide
|
|
73
|
+
any modifications that need to be applied to the class attributes.
|
|
72
74
|
* An example `slot_gatherer` is included.
|
|
73
75
|
2. Code generators that can make use of the gathered `Field`s to create magic method
|
|
74
|
-
source code.
|
|
75
|
-
* Example `
|
|
76
|
+
source code. To be made into descriptors by `MethodMaker`.
|
|
77
|
+
* Example `init_generator`, `repr_generator` and `eq_generator` generators are included.
|
|
76
78
|
3. A function that calls the `builder` function to apply both of these steps.
|
|
77
79
|
|
|
78
80
|
A field gathering function needs to take the original class as an argument and
|
|
@@ -90,25 +92,26 @@ class[^1] where keyword arguments define the names and values for the fields.
|
|
|
90
92
|
|
|
91
93
|
Code generator functions need to be converted to descriptors before being used.
|
|
92
94
|
This is done using the provided `MethodMaker` descriptor class.
|
|
93
|
-
ex: `
|
|
95
|
+
ex: `init_maker = MethodMaker("__init__", init_generator)`.
|
|
94
96
|
|
|
95
97
|
These parts can then be used to make a basic class boilerplate generator by
|
|
96
98
|
providing them to the `builder` function.
|
|
97
99
|
|
|
98
100
|
```python
|
|
99
101
|
from ducktools.classbuilder import (
|
|
100
|
-
builder,
|
|
101
|
-
slot_gatherer,
|
|
102
|
-
|
|
102
|
+
builder,
|
|
103
|
+
slot_gatherer,
|
|
104
|
+
init_generator, eq_generator, repr_generator,
|
|
103
105
|
MethodMaker,
|
|
104
106
|
)
|
|
105
107
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
init_maker = MethodMaker("__init__", init_generator)
|
|
109
|
+
repr_maker = MethodMaker("__repr__", repr_generator)
|
|
110
|
+
eq_maker = MethodMaker("__eq__", eq_generator)
|
|
111
|
+
|
|
109
112
|
|
|
110
113
|
def slotclass(cls):
|
|
111
|
-
return builder(cls, gatherer=slot_gatherer, methods={
|
|
114
|
+
return builder(cls, gatherer=slot_gatherer, methods={init_maker, repr_maker, eq_maker})
|
|
112
115
|
```
|
|
113
116
|
|
|
114
117
|
## Slot Class Usage ##
|
|
@@ -121,7 +124,7 @@ a similar manner to the `@dataclass` decorator from `dataclasses`.
|
|
|
121
124
|
> be used directly. (The included version has some extra features).
|
|
122
125
|
|
|
123
126
|
```python
|
|
124
|
-
from ducktools.classbuilder import Field, SlotFields
|
|
127
|
+
from ducktools.classbuilder import Field, SlotFields, slotclass
|
|
125
128
|
|
|
126
129
|
@slotclass
|
|
127
130
|
class SlottedDC:
|
|
@@ -206,6 +209,25 @@ print(f"{DataCoords is class_register[DataCoords.__name__] = }")
|
|
|
206
209
|
print(f"{SlotCoords is class_register[SlotCoords.__name__] = }")
|
|
207
210
|
```
|
|
208
211
|
|
|
212
|
+
## Using annotations anyway ##
|
|
213
|
+
|
|
214
|
+
For those that really want to use type annotations a basic `annotation_gatherer`
|
|
215
|
+
function and `@annotationclass` decorator are also included. Slots are not generated
|
|
216
|
+
in this case.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from ducktools.classbuilder import annotationclass
|
|
220
|
+
|
|
221
|
+
@annotationclass
|
|
222
|
+
class AnnotatedDC:
|
|
223
|
+
the_answer: int = 42
|
|
224
|
+
the_question: str = "What do you get if you multiply six by nine?"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
ex = AnnotatedDC()
|
|
228
|
+
print(ex)
|
|
229
|
+
```
|
|
230
|
+
|
|
209
231
|
## What features does this have? ##
|
|
210
232
|
|
|
211
233
|
Included as an example implementation, the `slotclass` generator supports
|
|
@@ -218,6 +240,9 @@ It will copy values provided as the `type` to `Field` into the
|
|
|
218
240
|
Values provided to `doc` will be placed in the final `__slots__`
|
|
219
241
|
field so they are present on the class if `help(...)` is called.
|
|
220
242
|
|
|
243
|
+
A fairly basic `annotations_gatherer` and `annotationclass` are also included
|
|
244
|
+
and can be used to generate classbuilders that rely on annotations.
|
|
245
|
+
|
|
221
246
|
If you want something with more features you can look at the `prefab.py`
|
|
222
247
|
implementation which provides a 'prebuilt' implementation.
|
|
223
248
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
ducktools/classbuilder/__init__.py,sha256=aB88mFw6bv5wzlZY88ZoCiWic3YMW3naxXmPhwT_fbs,20045
|
|
2
|
+
ducktools/classbuilder/__init__.pyi,sha256=rFNQYj_TeikQMGV-NCDD4fL9l9od7NkGRoIIyABItoQ,4310
|
|
3
|
+
ducktools/classbuilder/prefab.py,sha256=od-GEAYQokPDUePMOb2zPFGfh5p6zToJ-oEYpZx5CLA,28017
|
|
4
|
+
ducktools/classbuilder/prefab.pyi,sha256=f7GWVTyuQUU6EO6l0eDoiKy4uUq0DxK2ndHhJ0AGeKk,3830
|
|
5
|
+
ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
|
|
6
|
+
ducktools_classbuilder-0.5.0.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
|
|
7
|
+
ducktools_classbuilder-0.5.0.dist-info/METADATA,sha256=50Jn1_0FD2BadATbyBWOQTz3fIxV1cJ36J3frvp8qBY,10143
|
|
8
|
+
ducktools_classbuilder-0.5.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
9
|
+
ducktools_classbuilder-0.5.0.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
|
|
10
|
+
ducktools_classbuilder-0.5.0.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
ducktools/classbuilder/__init__.py,sha256=Vw5RjzzIkXGYXKOmFWTxPsIAM4ceg74Q1Oku4fwF0ek,13505
|
|
2
|
-
ducktools/classbuilder/__init__.pyi,sha256=hgcEylMAHgfonOt2fH9OCE3gU60mYkiOUe7e8mTwNVs,2857
|
|
3
|
-
ducktools/classbuilder/prefab.py,sha256=gJJCtTAbQLgIoo79jRWVp09r3u8kJjcqXp4G4FTu9j8,29887
|
|
4
|
-
ducktools/classbuilder/prefab.pyi,sha256=vcaEpBxyVsOTwkGBkVgmbpn8DPqQqFnAU4xTKPnpOd8,3977
|
|
5
|
-
ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
|
|
6
|
-
ducktools_classbuilder-0.3.0.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
|
|
7
|
-
ducktools_classbuilder-0.3.0.dist-info/METADATA,sha256=sAWw76LgN3HruCS-SXURrrAj2A6MmhsyDJwMSvnWW2w,9280
|
|
8
|
-
ducktools_classbuilder-0.3.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
9
|
-
ducktools_classbuilder-0.3.0.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
|
|
10
|
-
ducktools_classbuilder-0.3.0.dist-info/RECORD,,
|
{ducktools_classbuilder-0.3.0.dist-info → ducktools_classbuilder-0.5.0.dist-info}/LICENSE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.3.0.dist-info → ducktools_classbuilder-0.5.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|