ducktools-classbuilder 0.2.1__py3-none-any.whl → 0.4.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 +269 -104
- ducktools/classbuilder/__init__.pyi +46 -8
- ducktools/classbuilder/prefab.py +23 -88
- ducktools/classbuilder/prefab.pyi +1 -9
- {ducktools_classbuilder-0.2.1.dist-info → ducktools_classbuilder-0.4.0.dist-info}/METADATA +26 -2
- ducktools_classbuilder-0.4.0.dist-info/RECORD +10 -0
- ducktools_classbuilder-0.2.1.dist-info/RECORD +0 -10
- {ducktools_classbuilder-0.2.1.dist-info → ducktools_classbuilder-0.4.0.dist-info}/LICENSE.md +0 -0
- {ducktools_classbuilder-0.2.1.dist-info → ducktools_classbuilder-0.4.0.dist-info}/WHEEL +0 -0
- {ducktools_classbuilder-0.2.1.dist-info → ducktools_classbuilder-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -19,32 +19,17 @@
|
|
|
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.4.0"
|
|
23
25
|
|
|
24
26
|
# Change this name if you make heavy modifications
|
|
25
27
|
INTERNALS_DICT = "__classbuilder_internals__"
|
|
26
28
|
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
or return None.
|
|
32
|
-
|
|
33
|
-
As generated classes will always have 'fields'
|
|
34
|
-
and 'local_fields' attributes this will always
|
|
35
|
-
evaluate as 'truthy' if this is a generated class.
|
|
36
|
-
|
|
37
|
-
Generally you should use the helper get_flags and
|
|
38
|
-
get_fields methods.
|
|
39
|
-
|
|
40
|
-
Usage:
|
|
41
|
-
if internals := get_internals(cls):
|
|
42
|
-
...
|
|
43
|
-
|
|
44
|
-
:param cls: generated class
|
|
45
|
-
:return: internals dictionary of the class or None
|
|
46
|
-
"""
|
|
47
|
-
return getattr(cls, INTERNALS_DICT, None)
|
|
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
|
|
48
33
|
|
|
49
34
|
|
|
50
35
|
def get_fields(cls, *, local=False):
|
|
@@ -71,7 +56,9 @@ def get_flags(cls):
|
|
|
71
56
|
return getattr(cls, INTERNALS_DICT)["flags"]
|
|
72
57
|
|
|
73
58
|
|
|
74
|
-
def
|
|
59
|
+
def _get_inst_fields(inst):
|
|
60
|
+
# This is an internal helper for constructing new
|
|
61
|
+
# 'Field' instances from existing ones.
|
|
75
62
|
return {
|
|
76
63
|
k: getattr(inst, k)
|
|
77
64
|
for k in get_fields(type(inst))
|
|
@@ -105,7 +92,7 @@ class MethodMaker:
|
|
|
105
92
|
self.code_generator = code_generator
|
|
106
93
|
|
|
107
94
|
def __repr__(self):
|
|
108
|
-
return f"<MethodMaker for {self.funcname} method>"
|
|
95
|
+
return f"<MethodMaker for {self.funcname!r} method>"
|
|
109
96
|
|
|
110
97
|
def __get__(self, instance, cls):
|
|
111
98
|
local_vars = {}
|
|
@@ -122,37 +109,52 @@ class MethodMaker:
|
|
|
122
109
|
return method.__get__(instance, cls)
|
|
123
110
|
|
|
124
111
|
|
|
125
|
-
def
|
|
126
|
-
|
|
127
|
-
|
|
112
|
+
def get_init_maker(null=NOTHING, extra_code=None):
|
|
113
|
+
def cls_init_maker(cls):
|
|
114
|
+
fields = get_fields(cls)
|
|
115
|
+
flags = get_flags(cls)
|
|
128
116
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
117
|
+
arglist = []
|
|
118
|
+
assignments = []
|
|
119
|
+
globs = {}
|
|
132
120
|
|
|
133
|
-
|
|
134
|
-
|
|
121
|
+
if flags.get("kw_only", False):
|
|
122
|
+
arglist.append("*")
|
|
135
123
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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}"
|
|
148
136
|
|
|
149
|
-
|
|
150
|
-
|
|
137
|
+
arglist.append(arg)
|
|
138
|
+
assignments.append(assignment)
|
|
151
139
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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"
|
|
151
|
+
|
|
152
|
+
return code, globs
|
|
153
|
+
|
|
154
|
+
return cls_init_maker
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
init_maker = get_init_maker()
|
|
156
158
|
|
|
157
159
|
|
|
158
160
|
def repr_maker(cls):
|
|
@@ -189,11 +191,54 @@ def eq_maker(cls):
|
|
|
189
191
|
return code, globs
|
|
190
192
|
|
|
191
193
|
|
|
194
|
+
def frozen_setattr_maker(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_maker(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
|
+
|
|
192
235
|
# As only the __get__ method refers to the class we can use the same
|
|
193
236
|
# Descriptor instances for every class.
|
|
194
237
|
init_desc = MethodMaker("__init__", init_maker)
|
|
195
238
|
repr_desc = MethodMaker("__repr__", repr_maker)
|
|
196
239
|
eq_desc = MethodMaker("__eq__", eq_maker)
|
|
240
|
+
frozen_setattr_desc = MethodMaker("__setattr__", frozen_setattr_maker)
|
|
241
|
+
frozen_delattr_desc = MethodMaker("__delattr__", frozen_delattr_maker)
|
|
197
242
|
default_methods = frozenset({init_desc, repr_desc, eq_desc})
|
|
198
243
|
|
|
199
244
|
|
|
@@ -255,6 +300,8 @@ class Field:
|
|
|
255
300
|
some metadata.
|
|
256
301
|
|
|
257
302
|
Intended to be extendable by subclasses for additional features.
|
|
303
|
+
|
|
304
|
+
Note: When run under `pytest`, Field instances are Frozen.
|
|
258
305
|
"""
|
|
259
306
|
__slots__ = {
|
|
260
307
|
"default": "Standard default value to be used for attributes with"
|
|
@@ -300,7 +347,7 @@ class Field:
|
|
|
300
347
|
:param kwargs: Additional keyword arguments for subclasses
|
|
301
348
|
:return: new field subclass instance
|
|
302
349
|
"""
|
|
303
|
-
argument_dict = {**
|
|
350
|
+
argument_dict = {**_get_inst_fields(fld), **kwargs}
|
|
304
351
|
|
|
305
352
|
return cls(**argument_dict)
|
|
306
353
|
|
|
@@ -314,14 +361,19 @@ _field_internal = {
|
|
|
314
361
|
"doc": Field(default=None),
|
|
315
362
|
}
|
|
316
363
|
|
|
364
|
+
_field_methods = {repr_desc, eq_desc}
|
|
365
|
+
if _UNDER_TESTING:
|
|
366
|
+
_field_methods.update({frozen_setattr_desc, frozen_delattr_desc})
|
|
367
|
+
|
|
317
368
|
builder(
|
|
318
369
|
Field,
|
|
319
370
|
gatherer=lambda cls_: _field_internal,
|
|
320
|
-
methods=
|
|
371
|
+
methods=_field_methods,
|
|
321
372
|
flags={"slotted": True, "kw_only": True},
|
|
322
373
|
)
|
|
323
374
|
|
|
324
375
|
|
|
376
|
+
# Slot gathering tools
|
|
325
377
|
# Subclass of dict to be identifiable by isinstance checks
|
|
326
378
|
# For anything more complicated this could be made into a Mapping
|
|
327
379
|
class SlotFields(dict):
|
|
@@ -337,46 +389,152 @@ class SlotFields(dict):
|
|
|
337
389
|
"""
|
|
338
390
|
|
|
339
391
|
|
|
340
|
-
def
|
|
392
|
+
def make_slot_gatherer(field_type=Field):
|
|
393
|
+
def field_slot_gatherer(cls):
|
|
394
|
+
"""
|
|
395
|
+
Gather field information for class generation based on __slots__
|
|
396
|
+
|
|
397
|
+
:param cls: Class to gather field information from
|
|
398
|
+
:return: dict of field_name: Field(...)
|
|
399
|
+
"""
|
|
400
|
+
cls_slots = cls.__dict__.get("__slots__", None)
|
|
401
|
+
|
|
402
|
+
if not isinstance(cls_slots, SlotFields):
|
|
403
|
+
raise TypeError(
|
|
404
|
+
"__slots__ must be an instance of SlotFields "
|
|
405
|
+
"in order to generate a slotclass"
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
cls_annotations = cls.__dict__.get("__annotations__", {})
|
|
409
|
+
cls_fields = {}
|
|
410
|
+
slot_replacement = {}
|
|
411
|
+
|
|
412
|
+
for k, v in cls_slots.items():
|
|
413
|
+
if isinstance(v, field_type):
|
|
414
|
+
attrib = v
|
|
415
|
+
if attrib.type is not NOTHING:
|
|
416
|
+
cls_annotations[k] = attrib.type
|
|
417
|
+
else:
|
|
418
|
+
# Plain values treated as defaults
|
|
419
|
+
attrib = field_type(default=v)
|
|
420
|
+
|
|
421
|
+
slot_replacement[k] = attrib.doc
|
|
422
|
+
cls_fields[k] = attrib
|
|
423
|
+
|
|
424
|
+
# Replace the SlotAttributes instance with a regular dict
|
|
425
|
+
# So that help() works
|
|
426
|
+
setattr(cls, "__slots__", slot_replacement)
|
|
427
|
+
|
|
428
|
+
# Update annotations with any types from the slots assignment
|
|
429
|
+
setattr(cls, "__annotations__", cls_annotations)
|
|
430
|
+
return cls_fields
|
|
431
|
+
|
|
432
|
+
return field_slot_gatherer
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
slot_gatherer = make_slot_gatherer()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# Annotation gathering tools
|
|
439
|
+
def is_classvar(hint):
|
|
440
|
+
_typing = sys.modules.get("typing")
|
|
441
|
+
|
|
442
|
+
if _typing:
|
|
443
|
+
# Annotated is a nightmare I'm never waking up from
|
|
444
|
+
# 3.8 and 3.9 need Annotated from typing_extensions
|
|
445
|
+
# 3.8 also needs get_origin from typing_extensions
|
|
446
|
+
if sys.version_info < (3, 10):
|
|
447
|
+
_typing_extensions = sys.modules.get("typing_extensions")
|
|
448
|
+
if _typing_extensions:
|
|
449
|
+
_Annotated = _typing_extensions.Annotated
|
|
450
|
+
_get_origin = _typing_extensions.get_origin
|
|
451
|
+
else:
|
|
452
|
+
_Annotated, _get_origin = None, None
|
|
453
|
+
else:
|
|
454
|
+
_Annotated = _typing.Annotated
|
|
455
|
+
_get_origin = _typing.get_origin
|
|
456
|
+
|
|
457
|
+
if _Annotated and _get_origin(hint) is _Annotated:
|
|
458
|
+
hint = getattr(hint, "__origin__", None)
|
|
459
|
+
|
|
460
|
+
if (
|
|
461
|
+
hint is _typing.ClassVar
|
|
462
|
+
or getattr(hint, "__origin__", None) is _typing.ClassVar
|
|
463
|
+
):
|
|
464
|
+
return True
|
|
465
|
+
# String used as annotation
|
|
466
|
+
elif isinstance(hint, str) and "ClassVar" in hint:
|
|
467
|
+
return True
|
|
468
|
+
return False
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def make_annotation_gatherer(field_type=Field, leave_default_values=True):
|
|
341
472
|
"""
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
:
|
|
473
|
+
Create a new annotation gatherer that will work with `Field` instances
|
|
474
|
+
of the creators definition.
|
|
475
|
+
|
|
476
|
+
:param field_type: The `Field` classes to be used when gathering fields
|
|
477
|
+
:param leave_default_values: Set to True if the gatherer should leave
|
|
478
|
+
default values in place as class variables.
|
|
479
|
+
:return: An annotation gatherer with these settings.
|
|
346
480
|
"""
|
|
347
|
-
|
|
481
|
+
def field_annotation_gatherer(cls):
|
|
482
|
+
cls_annotations = cls.__dict__.get("__annotations__", {})
|
|
483
|
+
|
|
484
|
+
cls_fields: dict[str, field_type] = {}
|
|
485
|
+
|
|
486
|
+
for k, v in cls_annotations.items():
|
|
487
|
+
# Ignore ClassVar
|
|
488
|
+
if is_classvar(v):
|
|
489
|
+
continue
|
|
490
|
+
|
|
491
|
+
attrib = getattr(cls, k, NOTHING)
|
|
492
|
+
|
|
493
|
+
if attrib is not NOTHING:
|
|
494
|
+
if isinstance(attrib, field_type):
|
|
495
|
+
attrib = field_type.from_field(attrib, type=v)
|
|
496
|
+
if attrib.default is not NOTHING and leave_default_values:
|
|
497
|
+
setattr(cls, k, attrib.default)
|
|
498
|
+
else:
|
|
499
|
+
delattr(cls, k)
|
|
500
|
+
else:
|
|
501
|
+
attrib = field_type(default=attrib, type=v)
|
|
502
|
+
if not leave_default_values:
|
|
503
|
+
delattr(cls, k)
|
|
348
504
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
"__slots__ must be an instance of SlotFields "
|
|
352
|
-
"in order to generate a slotclass"
|
|
353
|
-
)
|
|
505
|
+
else:
|
|
506
|
+
attrib = field_type(type=v)
|
|
354
507
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
508
|
+
cls_fields[k] = attrib
|
|
509
|
+
|
|
510
|
+
return cls_fields
|
|
511
|
+
|
|
512
|
+
return field_annotation_gatherer
|
|
358
513
|
|
|
359
|
-
for k, v in cls_slots.items():
|
|
360
|
-
if isinstance(v, Field):
|
|
361
|
-
attrib = v
|
|
362
|
-
if v.type is not NOTHING:
|
|
363
|
-
cls_annotations[k] = attrib.type
|
|
364
|
-
else:
|
|
365
|
-
# Plain values treated as defaults
|
|
366
|
-
attrib = Field(default=v)
|
|
367
514
|
|
|
368
|
-
|
|
369
|
-
cls_fields[k] = attrib
|
|
515
|
+
annotation_gatherer = make_annotation_gatherer()
|
|
370
516
|
|
|
371
|
-
# Replace the SlotAttributes instance with a regular dict
|
|
372
|
-
# So that help() works
|
|
373
|
-
setattr(cls, "__slots__", slot_replacement)
|
|
374
517
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
518
|
+
def check_argument_order(cls):
|
|
519
|
+
"""
|
|
520
|
+
Raise a SyntaxError if the argument order will be invalid for a generated
|
|
521
|
+
`__init__` function.
|
|
522
|
+
|
|
523
|
+
:param cls: class being built
|
|
524
|
+
"""
|
|
525
|
+
fields = get_fields(cls)
|
|
526
|
+
used_default = False
|
|
527
|
+
for k, v in fields.items():
|
|
528
|
+
if v.default is NOTHING and v.default_factory is NOTHING:
|
|
529
|
+
if used_default:
|
|
530
|
+
raise SyntaxError(
|
|
531
|
+
f"non-default argument {k!r} follows default argument"
|
|
532
|
+
)
|
|
533
|
+
else:
|
|
534
|
+
used_default = True
|
|
378
535
|
|
|
379
536
|
|
|
537
|
+
# Class Decorators
|
|
380
538
|
def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
|
|
381
539
|
"""
|
|
382
540
|
Example of class builder in action using __slots__ to find fields.
|
|
@@ -393,43 +551,50 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
|
|
|
393
551
|
cls = builder(cls, gatherer=slot_gatherer, methods=methods, flags={"slotted": True})
|
|
394
552
|
|
|
395
553
|
if syntax_check:
|
|
396
|
-
|
|
397
|
-
used_default = False
|
|
398
|
-
for k, v in fields.items():
|
|
399
|
-
if v.default is NOTHING and v.default_factory is NOTHING:
|
|
400
|
-
if used_default:
|
|
401
|
-
raise SyntaxError(
|
|
402
|
-
f"non-default argument {k!r} follows default argument"
|
|
403
|
-
)
|
|
404
|
-
else:
|
|
405
|
-
used_default = True
|
|
554
|
+
check_argument_order(cls)
|
|
406
555
|
|
|
407
556
|
return cls
|
|
408
557
|
|
|
409
558
|
|
|
410
|
-
def
|
|
559
|
+
def annotationclass(cls=None, /, *, methods=default_methods):
|
|
560
|
+
if not cls:
|
|
561
|
+
return lambda cls_: annotationclass(cls_, methods=methods)
|
|
562
|
+
|
|
563
|
+
cls = builder(cls, gatherer=annotation_gatherer, methods=methods, flags={"slotted": False})
|
|
564
|
+
|
|
565
|
+
check_argument_order(cls)
|
|
566
|
+
|
|
567
|
+
return cls
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
_field_init_desc = MethodMaker(
|
|
571
|
+
funcname="__init__",
|
|
572
|
+
code_generator=get_init_maker(
|
|
573
|
+
null=_NothingType(),
|
|
574
|
+
extra_code=["self.validate_field()"],
|
|
575
|
+
)
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def fieldclass(cls=None, /, *, frozen=False):
|
|
411
580
|
"""
|
|
412
581
|
This is a special decorator for making Field subclasses using __slots__.
|
|
413
582
|
This works by forcing the __init__ method to treat NOTHING as a regular
|
|
414
583
|
value. This means *all* instance attributes always have defaults.
|
|
415
584
|
|
|
416
585
|
:param cls: Field subclass
|
|
586
|
+
:param frozen: Make the field class a frozen class.
|
|
587
|
+
Field classes are always frozen when running under `pytest`
|
|
417
588
|
:return: Modified subclass
|
|
418
589
|
"""
|
|
590
|
+
if not cls:
|
|
591
|
+
return lambda cls_: fieldclass(cls_, frozen=frozen)
|
|
419
592
|
|
|
420
|
-
|
|
421
|
-
# So append it to the code from __init__.
|
|
422
|
-
def field_init_func(cls_):
|
|
423
|
-
code, globs = init_maker(cls_, null=field_nothing)
|
|
424
|
-
code += " self.validate_field()\n"
|
|
425
|
-
return code, globs
|
|
593
|
+
field_methods = {_field_init_desc, repr_desc, eq_desc}
|
|
426
594
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
field_init_func,
|
|
431
|
-
)
|
|
432
|
-
field_methods = frozenset({field_init_desc, repr_desc, eq_desc})
|
|
595
|
+
# Always freeze when running tests
|
|
596
|
+
if frozen or _UNDER_TESTING:
|
|
597
|
+
field_methods.update({frozen_setattr_desc, frozen_delattr_desc})
|
|
433
598
|
|
|
434
599
|
cls = builder(
|
|
435
600
|
cls,
|
|
@@ -6,13 +6,11 @@ _py_type = type # Alias for type where it is used as a name
|
|
|
6
6
|
__version__: str
|
|
7
7
|
INTERNALS_DICT: str
|
|
8
8
|
|
|
9
|
-
def get_internals(cls) -> dict[str, typing.Any] | None: ...
|
|
10
|
-
|
|
11
9
|
def get_fields(cls: type, *, local: bool = False) -> dict[str, Field]: ...
|
|
12
10
|
|
|
13
11
|
def get_flags(cls:type) -> dict[str, bool]: ...
|
|
14
12
|
|
|
15
|
-
def
|
|
13
|
+
def _get_inst_fields(inst: typing.Any) -> dict[str, typing.Any]: ...
|
|
16
14
|
|
|
17
15
|
class _NothingType:
|
|
18
16
|
...
|
|
@@ -28,17 +26,24 @@ class MethodMaker:
|
|
|
28
26
|
def __repr__(self) -> str: ...
|
|
29
27
|
def __get__(self, instance, cls) -> Callable: ...
|
|
30
28
|
|
|
31
|
-
def
|
|
32
|
-
cls: type,
|
|
33
|
-
*,
|
|
29
|
+
def get_init_maker(
|
|
34
30
|
null: _NothingType = NOTHING,
|
|
35
|
-
|
|
31
|
+
extra_code: None | list[str] = None
|
|
32
|
+
) -> Callable[[type], tuple[str, dict[str, typing.Any]]]: ...
|
|
33
|
+
|
|
34
|
+
def init_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
|
|
36
35
|
def repr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
|
|
37
36
|
def eq_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
|
|
38
37
|
|
|
38
|
+
def frozen_setattr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
|
|
39
|
+
|
|
40
|
+
def frozen_delattr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
|
|
41
|
+
|
|
39
42
|
init_desc: MethodMaker
|
|
40
43
|
repr_desc: MethodMaker
|
|
41
44
|
eq_desc: MethodMaker
|
|
45
|
+
frozen_setattr_desc: MethodMaker
|
|
46
|
+
frozen_delattr_desc: MethodMaker
|
|
42
47
|
default_methods: frozenset[MethodMaker]
|
|
43
48
|
|
|
44
49
|
_T = typing.TypeVar("_T")
|
|
@@ -90,9 +95,22 @@ class Field:
|
|
|
90
95
|
class SlotFields(dict):
|
|
91
96
|
...
|
|
92
97
|
|
|
98
|
+
def make_slot_gatherer(field_type: type[Field] = Field) -> Callable[[type], dict[str, Field]]: ...
|
|
99
|
+
|
|
93
100
|
def slot_gatherer(cls: type) -> dict[str, Field]:
|
|
94
101
|
...
|
|
95
102
|
|
|
103
|
+
def is_classvar(hint: object) -> bool: ...
|
|
104
|
+
|
|
105
|
+
def make_annotation_gatherer(
|
|
106
|
+
field_type: type[Field] = Field,
|
|
107
|
+
leave_default_values: bool = True,
|
|
108
|
+
) -> Callable[[type], dict[str, Field]]: ...
|
|
109
|
+
|
|
110
|
+
def annotation_gatherer(cls: type) -> dict[str, Field]: ...
|
|
111
|
+
|
|
112
|
+
def check_argument_order(cls: type) -> None: ...
|
|
113
|
+
|
|
96
114
|
@typing.overload
|
|
97
115
|
def slotclass(
|
|
98
116
|
cls: type[_T],
|
|
@@ -111,4 +129,24 @@ def slotclass(
|
|
|
111
129
|
syntax_check: bool = True
|
|
112
130
|
) -> Callable[[type[_T]], type[_T]]: ...
|
|
113
131
|
|
|
114
|
-
|
|
132
|
+
@typing.overload
|
|
133
|
+
def annotationclass(
|
|
134
|
+
cls: type[_T],
|
|
135
|
+
/,
|
|
136
|
+
*,
|
|
137
|
+
methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
|
|
138
|
+
) -> type[_T]: ...
|
|
139
|
+
|
|
140
|
+
@typing.overload
|
|
141
|
+
def annotationclass(
|
|
142
|
+
cls: None = None,
|
|
143
|
+
/,
|
|
144
|
+
*,
|
|
145
|
+
methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
|
|
146
|
+
) -> Callable[[type[_T]], type[_T]]: ...
|
|
147
|
+
|
|
148
|
+
@typing.overload
|
|
149
|
+
def fieldclass(cls: type[_T], /, *, frozen: bool = False) -> type[_T]: ...
|
|
150
|
+
|
|
151
|
+
@typing.overload
|
|
152
|
+
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_desc, frozen_delattr_desc, 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)
|
|
@@ -414,8 +350,6 @@ repr_desc = get_repr_maker()
|
|
|
414
350
|
recursive_repr_desc = get_repr_maker(recursion_safe=True)
|
|
415
351
|
eq_desc = get_eq_maker()
|
|
416
352
|
iter_desc = get_iter_maker()
|
|
417
|
-
frozen_setattr_desc = get_frozen_setattr_maker()
|
|
418
|
-
frozen_delattr_desc = get_frozen_delattr_maker()
|
|
419
353
|
asdict_desc = get_asdict_maker()
|
|
420
354
|
|
|
421
355
|
|
|
@@ -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
|
|
@@ -519,7 +448,7 @@ def attribute_gatherer(cls):
|
|
|
519
448
|
new_attributes = {}
|
|
520
449
|
for name, value in cls_annotations.items():
|
|
521
450
|
# Ignore ClassVar hints
|
|
522
|
-
if
|
|
451
|
+
if is_classvar(value):
|
|
523
452
|
continue
|
|
524
453
|
|
|
525
454
|
# Look for the KW_ONLY annotation
|
|
@@ -533,37 +462,43 @@ def attribute_gatherer(cls):
|
|
|
533
462
|
# Copy attributes that are already defined to the new dict
|
|
534
463
|
# generate Attribute() values for those that are not defined.
|
|
535
464
|
|
|
465
|
+
# Extra parameters to pass to each Attribute
|
|
466
|
+
extras = {
|
|
467
|
+
"type": cls_annotations[name]
|
|
468
|
+
}
|
|
469
|
+
if kw_flag:
|
|
470
|
+
extras["kw_only"] = True
|
|
471
|
+
|
|
536
472
|
# If a field name is also declared in slots it can't have a real
|
|
537
473
|
# default value and the attr will be the slot descriptor.
|
|
538
474
|
if hasattr(cls, name) and name not in cls_slots:
|
|
539
475
|
if name in cls_attribute_names:
|
|
540
|
-
attrib =
|
|
476
|
+
attrib = Attribute.from_field(
|
|
477
|
+
cls_attributes[name],
|
|
478
|
+
**extras,
|
|
479
|
+
)
|
|
541
480
|
else:
|
|
542
481
|
attribute_default = getattr(cls, name)
|
|
543
|
-
attrib = attribute(default=attribute_default)
|
|
482
|
+
attrib = attribute(default=attribute_default, **extras)
|
|
544
483
|
|
|
545
484
|
# Clear the attribute from the class after it has been used
|
|
546
485
|
# in the definition.
|
|
547
486
|
delattr(cls, name)
|
|
548
487
|
else:
|
|
549
|
-
attrib = attribute()
|
|
488
|
+
attrib = attribute(**extras)
|
|
550
489
|
|
|
551
|
-
if kw_flag:
|
|
552
|
-
attrib.kw_only = True
|
|
553
|
-
|
|
554
|
-
attrib.type = cls_annotations[name]
|
|
555
490
|
new_attributes[name] = attrib
|
|
556
491
|
|
|
557
492
|
cls_attributes = new_attributes
|
|
558
493
|
else:
|
|
559
|
-
for name
|
|
560
|
-
|
|
494
|
+
for name in cls_attributes.keys():
|
|
495
|
+
attrib = cls_attributes[name]
|
|
496
|
+
delattr(cls, name) # clear attrib from class
|
|
561
497
|
|
|
562
498
|
# Some items can still be annotated.
|
|
563
|
-
|
|
564
|
-
attrib
|
|
565
|
-
|
|
566
|
-
pass
|
|
499
|
+
if name in cls_annotations:
|
|
500
|
+
new_attrib = Attribute.from_field(attrib, type=cls_annotations[name])
|
|
501
|
+
cls_attributes[name] = new_attrib
|
|
567
502
|
|
|
568
503
|
return cls_attributes
|
|
569
504
|
|
|
@@ -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,
|
|
9
|
+
builder, fieldclass, get_flags, get_fields, 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,10 +36,6 @@ 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
|
|
|
@@ -51,8 +45,6 @@ repr_desc: MethodMaker
|
|
|
51
45
|
recursive_repr_desc: MethodMaker
|
|
52
46
|
eq_desc: MethodMaker
|
|
53
47
|
iter_desc: MethodMaker
|
|
54
|
-
frozen_setattr_desc: MethodMaker
|
|
55
|
-
frozen_delattr_desc: MethodMaker
|
|
56
48
|
asdict_desc: MethodMaker
|
|
57
49
|
|
|
58
50
|
class Attribute(Field):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ducktools-classbuilder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
|
@@ -121,7 +122,7 @@ a similar manner to the `@dataclass` decorator from `dataclasses`.
|
|
|
121
122
|
> be used directly. (The included version has some extra features).
|
|
122
123
|
|
|
123
124
|
```python
|
|
124
|
-
from ducktools.classbuilder import Field, SlotFields
|
|
125
|
+
from ducktools.classbuilder import Field, SlotFields, slotclass
|
|
125
126
|
|
|
126
127
|
@slotclass
|
|
127
128
|
class SlottedDC:
|
|
@@ -206,6 +207,25 @@ print(f"{DataCoords is class_register[DataCoords.__name__] = }")
|
|
|
206
207
|
print(f"{SlotCoords is class_register[SlotCoords.__name__] = }")
|
|
207
208
|
```
|
|
208
209
|
|
|
210
|
+
## Using annotations anyway ##
|
|
211
|
+
|
|
212
|
+
For those that really want to use type annotations a basic `annotation_gatherer`
|
|
213
|
+
function and `@annotationclass` decorator are also included. Slots are not generated
|
|
214
|
+
in this case.
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from ducktools.classbuilder import annotationclass
|
|
218
|
+
|
|
219
|
+
@annotationclass
|
|
220
|
+
class AnnotatedDC:
|
|
221
|
+
the_answer: int = 42
|
|
222
|
+
the_question: str = "What do you get if you multiply six by nine?"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
ex = AnnotatedDC()
|
|
226
|
+
print(ex)
|
|
227
|
+
```
|
|
228
|
+
|
|
209
229
|
## What features does this have? ##
|
|
210
230
|
|
|
211
231
|
Included as an example implementation, the `slotclass` generator supports
|
|
@@ -218,6 +238,10 @@ It will copy values provided as the `type` to `Field` into the
|
|
|
218
238
|
Values provided to `doc` will be placed in the final `__slots__`
|
|
219
239
|
field so they are present on the class if `help(...)` is called.
|
|
220
240
|
|
|
241
|
+
A fairly basic `annotations_gatherer` and `annotationclass` are included
|
|
242
|
+
in `extras.py` which can be used to generate classbuilders that rely on
|
|
243
|
+
annotations.
|
|
244
|
+
|
|
221
245
|
If you want something with more features you can look at the `prefab.py`
|
|
222
246
|
implementation which provides a 'prebuilt' implementation.
|
|
223
247
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
ducktools/classbuilder/__init__.py,sha256=7OkqvfHLSSJylkAeiErgZXB9E-lDUK-rvbu3CRWFSdc,19113
|
|
2
|
+
ducktools/classbuilder/__init__.pyi,sha256=64cp1-zpOh2bUVlwcoTTdaampFINIhdFbFDcleE3N0g,4091
|
|
3
|
+
ducktools/classbuilder/prefab.py,sha256=9K_02AJ3-gLR0Cb0RxKwd2n_cwVtE2mf1TE2n_VNp4o,27947
|
|
4
|
+
ducktools/classbuilder/prefab.pyi,sha256=5MiPuuT4hc-Er4xFfrEkBqz-dCjULYJfTM_c0upa0Dc,3758
|
|
5
|
+
ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
|
|
6
|
+
ducktools_classbuilder-0.4.0.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
|
|
7
|
+
ducktools_classbuilder-0.4.0.dist-info/METADATA,sha256=teRrGNNsVoQJ1jLnzctDVFREtSDq9MVCS9GNZr1S01E,9982
|
|
8
|
+
ducktools_classbuilder-0.4.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
9
|
+
ducktools_classbuilder-0.4.0.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
|
|
10
|
+
ducktools_classbuilder-0.4.0.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
ducktools/classbuilder/__init__.py,sha256=yZOiy84vo8z0iotOgY1rFwBBngveh1cwoWjcQdRndpw,13747
|
|
2
|
-
ducktools/classbuilder/__init__.pyi,sha256=-QHJhPn4EjI4XW4OI74MKNg2tGKJiM3w0ELSOvPstAw,2877
|
|
3
|
-
ducktools/classbuilder/prefab.py,sha256=gJJCtTAbQLgIoo79jRWVp09r3u8kJjcqXp4G4FTu9j8,29887
|
|
4
|
-
ducktools/classbuilder/prefab.pyi,sha256=GsllqqZ_Wz6i_DenKv-pAbtES3sPanC0g8O7fkNTnvs,3969
|
|
5
|
-
ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
|
|
6
|
-
ducktools_classbuilder-0.2.1.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
|
|
7
|
-
ducktools_classbuilder-0.2.1.dist-info/METADATA,sha256=kV--mUdR1y0NmszwMobZdnNvigtepG_WF_axupjf6_4,9280
|
|
8
|
-
ducktools_classbuilder-0.2.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
9
|
-
ducktools_classbuilder-0.2.1.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
|
|
10
|
-
ducktools_classbuilder-0.2.1.dist-info/RECORD,,
|
{ducktools_classbuilder-0.2.1.dist-info → ducktools_classbuilder-0.4.0.dist-info}/LICENSE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.2.1.dist-info → ducktools_classbuilder-0.4.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|