ducktools-classbuilder 0.3.0__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 +264 -85
- ducktools/classbuilder/__init__.pyi +44 -5
- ducktools/classbuilder/prefab.py +23 -88
- ducktools/classbuilder/prefab.pyi +0 -8
- {ducktools_classbuilder-0.3.0.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.3.0.dist-info/RECORD +0 -10
- {ducktools_classbuilder-0.3.0.dist-info → ducktools_classbuilder-0.4.0.dist-info}/LICENSE.md +0 -0
- {ducktools_classbuilder-0.3.0.dist-info → ducktools_classbuilder-0.4.0.dist-info}/WHEEL +0 -0
- {ducktools_classbuilder-0.3.0.dist-info → ducktools_classbuilder-0.4.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.4.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
|
|
@@ -102,46 +109,52 @@ class MethodMaker:
|
|
|
102
109
|
return method.__get__(instance, cls)
|
|
103
110
|
|
|
104
111
|
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
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)
|
|
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_maker = get_init_maker()
|
|
145
158
|
|
|
146
159
|
|
|
147
160
|
def repr_maker(cls):
|
|
@@ -178,11 +191,54 @@ def eq_maker(cls):
|
|
|
178
191
|
return code, globs
|
|
179
192
|
|
|
180
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
|
+
|
|
181
235
|
# As only the __get__ method refers to the class we can use the same
|
|
182
236
|
# Descriptor instances for every class.
|
|
183
237
|
init_desc = MethodMaker("__init__", init_maker)
|
|
184
238
|
repr_desc = MethodMaker("__repr__", repr_maker)
|
|
185
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)
|
|
186
242
|
default_methods = frozenset({init_desc, repr_desc, eq_desc})
|
|
187
243
|
|
|
188
244
|
|
|
@@ -244,6 +300,8 @@ class Field:
|
|
|
244
300
|
some metadata.
|
|
245
301
|
|
|
246
302
|
Intended to be extendable by subclasses for additional features.
|
|
303
|
+
|
|
304
|
+
Note: When run under `pytest`, Field instances are Frozen.
|
|
247
305
|
"""
|
|
248
306
|
__slots__ = {
|
|
249
307
|
"default": "Standard default value to be used for attributes with"
|
|
@@ -303,14 +361,19 @@ _field_internal = {
|
|
|
303
361
|
"doc": Field(default=None),
|
|
304
362
|
}
|
|
305
363
|
|
|
364
|
+
_field_methods = {repr_desc, eq_desc}
|
|
365
|
+
if _UNDER_TESTING:
|
|
366
|
+
_field_methods.update({frozen_setattr_desc, frozen_delattr_desc})
|
|
367
|
+
|
|
306
368
|
builder(
|
|
307
369
|
Field,
|
|
308
370
|
gatherer=lambda cls_: _field_internal,
|
|
309
|
-
methods=
|
|
371
|
+
methods=_field_methods,
|
|
310
372
|
flags={"slotted": True, "kw_only": True},
|
|
311
373
|
)
|
|
312
374
|
|
|
313
375
|
|
|
376
|
+
# Slot gathering tools
|
|
314
377
|
# Subclass of dict to be identifiable by isinstance checks
|
|
315
378
|
# For anything more complicated this could be made into a Mapping
|
|
316
379
|
class SlotFields(dict):
|
|
@@ -326,46 +389,152 @@ class SlotFields(dict):
|
|
|
326
389
|
"""
|
|
327
390
|
|
|
328
391
|
|
|
329
|
-
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):
|
|
330
472
|
"""
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
:
|
|
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.
|
|
335
480
|
"""
|
|
336
|
-
|
|
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)
|
|
337
504
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
"__slots__ must be an instance of SlotFields "
|
|
341
|
-
"in order to generate a slotclass"
|
|
342
|
-
)
|
|
505
|
+
else:
|
|
506
|
+
attrib = field_type(type=v)
|
|
343
507
|
|
|
344
|
-
|
|
345
|
-
cls_fields = {}
|
|
346
|
-
slot_replacement = {}
|
|
508
|
+
cls_fields[k] = attrib
|
|
347
509
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if v.type is not NOTHING:
|
|
352
|
-
cls_annotations[k] = attrib.type
|
|
353
|
-
else:
|
|
354
|
-
# Plain values treated as defaults
|
|
355
|
-
attrib = Field(default=v)
|
|
510
|
+
return cls_fields
|
|
511
|
+
|
|
512
|
+
return field_annotation_gatherer
|
|
356
513
|
|
|
357
|
-
slot_replacement[k] = attrib.doc
|
|
358
|
-
cls_fields[k] = attrib
|
|
359
514
|
|
|
360
|
-
|
|
361
|
-
# So that help() works
|
|
362
|
-
setattr(cls, "__slots__", slot_replacement)
|
|
515
|
+
annotation_gatherer = make_annotation_gatherer()
|
|
363
516
|
|
|
364
|
-
# Update annotations with any types from the slots assignment
|
|
365
|
-
setattr(cls, "__annotations__", cls_annotations)
|
|
366
|
-
return cls_fields
|
|
367
517
|
|
|
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
|
|
368
535
|
|
|
536
|
+
|
|
537
|
+
# Class Decorators
|
|
369
538
|
def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
|
|
370
539
|
"""
|
|
371
540
|
Example of class builder in action using __slots__ to find fields.
|
|
@@ -382,40 +551,50 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
|
|
|
382
551
|
cls = builder(cls, gatherer=slot_gatherer, methods=methods, flags={"slotted": True})
|
|
383
552
|
|
|
384
553
|
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
|
|
554
|
+
check_argument_order(cls)
|
|
395
555
|
|
|
396
556
|
return cls
|
|
397
557
|
|
|
398
558
|
|
|
399
|
-
def
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
field_nothing = _NothingType()
|
|
403
|
-
extra_calls = ["self.validate_field()"]
|
|
404
|
-
return init_maker(cls, null=field_nothing, extra_code=extra_calls)
|
|
559
|
+
def annotationclass(cls=None, /, *, methods=default_methods):
|
|
560
|
+
if not cls:
|
|
561
|
+
return lambda cls_: annotationclass(cls_, methods=methods)
|
|
405
562
|
|
|
563
|
+
cls = builder(cls, gatherer=annotation_gatherer, methods=methods, flags={"slotted": False})
|
|
406
564
|
|
|
407
|
-
|
|
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):
|
|
408
580
|
"""
|
|
409
581
|
This is a special decorator for making Field subclasses using __slots__.
|
|
410
582
|
This works by forcing the __init__ method to treat NOTHING as a regular
|
|
411
583
|
value. This means *all* instance attributes always have defaults.
|
|
412
584
|
|
|
413
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`
|
|
414
588
|
:return: Modified subclass
|
|
415
589
|
"""
|
|
590
|
+
if not cls:
|
|
591
|
+
return lambda cls_: fieldclass(cls_, frozen=frozen)
|
|
592
|
+
|
|
593
|
+
field_methods = {_field_init_desc, repr_desc, eq_desc}
|
|
416
594
|
|
|
417
|
-
|
|
418
|
-
|
|
595
|
+
# Always freeze when running tests
|
|
596
|
+
if frozen or _UNDER_TESTING:
|
|
597
|
+
field_methods.update({frozen_setattr_desc, frozen_delattr_desc})
|
|
419
598
|
|
|
420
599
|
cls = builder(
|
|
421
600
|
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_maker(
|
|
32
30
|
null: _NothingType = NOTHING,
|
|
33
31
|
extra_code: None | list[str] = None
|
|
34
|
-
) -> tuple[str, dict[str, typing.Any]]: ...
|
|
32
|
+
) -> Callable[[type], tuple[str, dict[str, typing.Any]]]: ...
|
|
33
|
+
|
|
34
|
+
def init_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
|
|
35
35
|
def repr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
|
|
36
36
|
def eq_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
|
|
37
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
|
+
|
|
38
42
|
init_desc: MethodMaker
|
|
39
43
|
repr_desc: MethodMaker
|
|
40
44
|
eq_desc: MethodMaker
|
|
45
|
+
frozen_setattr_desc: MethodMaker
|
|
46
|
+
frozen_delattr_desc: MethodMaker
|
|
41
47
|
default_methods: frozenset[MethodMaker]
|
|
42
48
|
|
|
43
49
|
_T = typing.TypeVar("_T")
|
|
@@ -89,9 +95,22 @@ class Field:
|
|
|
89
95
|
class SlotFields(dict):
|
|
90
96
|
...
|
|
91
97
|
|
|
98
|
+
def make_slot_gatherer(field_type: type[Field] = Field) -> Callable[[type], dict[str, Field]]: ...
|
|
99
|
+
|
|
92
100
|
def slot_gatherer(cls: type) -> dict[str, Field]:
|
|
93
101
|
...
|
|
94
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
|
+
|
|
95
114
|
@typing.overload
|
|
96
115
|
def slotclass(
|
|
97
116
|
cls: type[_T],
|
|
@@ -110,4 +129,24 @@ def slotclass(
|
|
|
110
129
|
syntax_check: bool = True
|
|
111
130
|
) -> Callable[[type[_T]], type[_T]]: ...
|
|
112
131
|
|
|
113
|
-
|
|
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
|
|
|
@@ -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=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.4.0.dist-info}/LICENSE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.3.0.dist-info → ducktools_classbuilder-0.4.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|