ducktools-classbuilder 0.1.0__py3-none-any.whl → 0.2.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 +35 -12
- ducktools/classbuilder/__init__.pyi +25 -22
- ducktools/classbuilder/prefab.py +46 -24
- ducktools/classbuilder/prefab.pyi +25 -13
- {ducktools_classbuilder-0.1.0.dist-info → ducktools_classbuilder-0.2.0.dist-info}/METADATA +89 -23
- ducktools_classbuilder-0.2.0.dist-info/RECORD +10 -0
- ducktools_classbuilder-0.1.0.dist-info/RECORD +0 -10
- {ducktools_classbuilder-0.1.0.dist-info → ducktools_classbuilder-0.2.0.dist-info}/LICENSE.md +0 -0
- {ducktools_classbuilder-0.1.0.dist-info → ducktools_classbuilder-0.2.0.dist-info}/WHEEL +0 -0
- {ducktools_classbuilder-0.1.0.dist-info → ducktools_classbuilder-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -19,7 +19,7 @@
|
|
|
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
|
-
__version__ = "v0.
|
|
22
|
+
__version__ = "v0.2.0"
|
|
23
23
|
|
|
24
24
|
# Change this name if you make heavy modifications
|
|
25
25
|
INTERNALS_DICT = "__classbuilder_internals__"
|
|
@@ -34,6 +34,9 @@ def get_internals(cls):
|
|
|
34
34
|
and 'local_fields' attributes this will always
|
|
35
35
|
evaluate as 'truthy' if this is a generated class.
|
|
36
36
|
|
|
37
|
+
Generally you should use the helper get_flags and
|
|
38
|
+
get_fields methods.
|
|
39
|
+
|
|
37
40
|
Usage:
|
|
38
41
|
if internals := get_internals(cls):
|
|
39
42
|
...
|
|
@@ -44,15 +47,28 @@ def get_internals(cls):
|
|
|
44
47
|
return getattr(cls, INTERNALS_DICT, None)
|
|
45
48
|
|
|
46
49
|
|
|
47
|
-
def get_fields(cls):
|
|
50
|
+
def get_fields(cls, *, local=False):
|
|
48
51
|
"""
|
|
49
52
|
Utility function to gather the fields dictionary
|
|
50
53
|
from the class internals.
|
|
51
54
|
|
|
52
55
|
:param cls: generated class
|
|
56
|
+
:param local: get only fields that were not inherited
|
|
53
57
|
:return: dictionary of keys and Field attribute info
|
|
54
58
|
"""
|
|
55
|
-
|
|
59
|
+
key = "local_fields" if local else "fields"
|
|
60
|
+
return getattr(cls, INTERNALS_DICT)[key]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_flags(cls):
|
|
64
|
+
"""
|
|
65
|
+
Utility function to gather the flags dictionary
|
|
66
|
+
from the class internals.
|
|
67
|
+
|
|
68
|
+
:param cls: generated class
|
|
69
|
+
:return: dictionary of keys and flag values
|
|
70
|
+
"""
|
|
71
|
+
return getattr(cls, INTERNALS_DICT)["flags"]
|
|
56
72
|
|
|
57
73
|
|
|
58
74
|
def get_inst_fields(inst):
|
|
@@ -106,14 +122,15 @@ class MethodMaker:
|
|
|
106
122
|
return method.__get__(instance, cls)
|
|
107
123
|
|
|
108
124
|
|
|
109
|
-
def init_maker(cls, *, null=NOTHING
|
|
125
|
+
def init_maker(cls, *, null=NOTHING):
|
|
110
126
|
fields = get_fields(cls)
|
|
127
|
+
flags = get_flags(cls)
|
|
111
128
|
|
|
112
129
|
arglist = []
|
|
113
130
|
assignments = []
|
|
114
131
|
globs = {}
|
|
115
132
|
|
|
116
|
-
if kw_only:
|
|
133
|
+
if flags.get("kw_only", False):
|
|
117
134
|
arglist.append("*")
|
|
118
135
|
|
|
119
136
|
for k, v in fields.items():
|
|
@@ -180,7 +197,7 @@ eq_desc = MethodMaker("__eq__", eq_maker)
|
|
|
180
197
|
default_methods = frozenset({init_desc, repr_desc, eq_desc})
|
|
181
198
|
|
|
182
199
|
|
|
183
|
-
def builder(cls=None, /, *, gatherer, methods):
|
|
200
|
+
def builder(cls=None, /, *, gatherer, methods, flags=None):
|
|
184
201
|
"""
|
|
185
202
|
The main builder for class generation
|
|
186
203
|
|
|
@@ -189,6 +206,8 @@ def builder(cls=None, /, *, gatherer, methods):
|
|
|
189
206
|
:type gatherer: Callable[[type], dict[str, Field]]
|
|
190
207
|
:param methods: MethodMakers to add to the class
|
|
191
208
|
:type methods: set[MethodMaker]
|
|
209
|
+
:param flags: additional flags to store in the internals dictionary
|
|
210
|
+
for use by method generators.
|
|
192
211
|
:return: The modified class (the class itself is modified, but this is expected).
|
|
193
212
|
"""
|
|
194
213
|
# Handle `None` to make wrapping with a decorator easier.
|
|
@@ -197,6 +216,7 @@ def builder(cls=None, /, *, gatherer, methods):
|
|
|
197
216
|
cls_,
|
|
198
217
|
gatherer=gatherer,
|
|
199
218
|
methods=methods,
|
|
219
|
+
flags=flags,
|
|
200
220
|
)
|
|
201
221
|
|
|
202
222
|
internals = {}
|
|
@@ -212,11 +232,12 @@ def builder(cls=None, /, *, gatherer, methods):
|
|
|
212
232
|
fields = {}
|
|
213
233
|
for c in reversed(mro):
|
|
214
234
|
try:
|
|
215
|
-
fields.update(
|
|
235
|
+
fields.update(get_fields(c, local=True))
|
|
216
236
|
except AttributeError:
|
|
217
237
|
pass
|
|
218
238
|
|
|
219
239
|
internals["fields"] = fields
|
|
240
|
+
internals["flags"] = flags if flags is not None else {}
|
|
220
241
|
|
|
221
242
|
# Assign all of the method generators
|
|
222
243
|
for method in methods:
|
|
@@ -296,7 +317,8 @@ _field_internal = {
|
|
|
296
317
|
builder(
|
|
297
318
|
Field,
|
|
298
319
|
gatherer=lambda cls_: _field_internal,
|
|
299
|
-
methods=frozenset({repr_desc, eq_desc})
|
|
320
|
+
methods=frozenset({repr_desc, eq_desc}),
|
|
321
|
+
flags={"slotted": True, "kw_only": True},
|
|
300
322
|
)
|
|
301
323
|
|
|
302
324
|
|
|
@@ -368,7 +390,7 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
|
|
|
368
390
|
if not cls:
|
|
369
391
|
return lambda cls_: slotclass(cls_, methods=methods, syntax_check=syntax_check)
|
|
370
392
|
|
|
371
|
-
cls = builder(cls, gatherer=slot_gatherer, methods=methods)
|
|
393
|
+
cls = builder(cls, gatherer=slot_gatherer, methods=methods, flags={"slotted": True})
|
|
372
394
|
|
|
373
395
|
if syntax_check:
|
|
374
396
|
fields = get_fields(cls)
|
|
@@ -398,7 +420,7 @@ def fieldclass(cls):
|
|
|
398
420
|
# Fields need a way to call their validate method
|
|
399
421
|
# So append it to the code from __init__.
|
|
400
422
|
def field_init_func(cls_):
|
|
401
|
-
code, globs = init_maker(cls_, null=field_nothing
|
|
423
|
+
code, globs = init_maker(cls_, null=field_nothing)
|
|
402
424
|
code += " self.validate_field()\n"
|
|
403
425
|
return code, globs
|
|
404
426
|
|
|
@@ -412,7 +434,8 @@ def fieldclass(cls):
|
|
|
412
434
|
cls = builder(
|
|
413
435
|
cls,
|
|
414
436
|
gatherer=slot_gatherer,
|
|
415
|
-
methods=field_methods
|
|
437
|
+
methods=field_methods,
|
|
438
|
+
flags={"slotted": True, "kw_only": True}
|
|
416
439
|
)
|
|
417
440
|
|
|
418
|
-
return cls
|
|
441
|
+
return cls
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
from collections.abc import Callable
|
|
3
3
|
|
|
4
|
+
_py_type = type # Alias for type where it is used as a name
|
|
5
|
+
|
|
4
6
|
__version__: str
|
|
5
7
|
INTERNALS_DICT: str
|
|
6
8
|
|
|
7
9
|
def get_internals(cls) -> dict[str, typing.Any] | None: ...
|
|
8
10
|
|
|
9
|
-
def get_fields(cls: type) -> dict[str, Field]: ...
|
|
11
|
+
def get_fields(cls: type, *, local: bool = False) -> dict[str, Field]: ...
|
|
12
|
+
|
|
13
|
+
def get_flags(cls:type) -> dict[str, bool]: ...
|
|
10
14
|
|
|
11
15
|
def get_inst_fields(inst: typing.Any) -> dict[str, typing.Any]: ...
|
|
12
16
|
|
|
@@ -28,7 +32,6 @@ def init_maker(
|
|
|
28
32
|
cls: type,
|
|
29
33
|
*,
|
|
30
34
|
null: _NothingType = NOTHING,
|
|
31
|
-
kw_only: bool = False
|
|
32
35
|
) -> tuple[str, dict[str, typing.Any]]: ...
|
|
33
36
|
def repr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
|
|
34
37
|
def eq_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
|
|
@@ -38,14 +41,17 @@ repr_desc: MethodMaker
|
|
|
38
41
|
eq_desc: MethodMaker
|
|
39
42
|
default_methods: frozenset[MethodMaker]
|
|
40
43
|
|
|
44
|
+
_T = typing.TypeVar("_T")
|
|
45
|
+
|
|
41
46
|
@typing.overload
|
|
42
47
|
def builder(
|
|
43
|
-
cls: type,
|
|
48
|
+
cls: type[_T],
|
|
44
49
|
/,
|
|
45
50
|
*,
|
|
46
51
|
gatherer: Callable[[type], dict[str, Field]],
|
|
47
|
-
methods: frozenset[MethodMaker] | set[MethodMaker]
|
|
48
|
-
|
|
52
|
+
methods: frozenset[MethodMaker] | set[MethodMaker],
|
|
53
|
+
flags: dict[str, bool] | None = None,
|
|
54
|
+
) -> type[_T]: ...
|
|
49
55
|
|
|
50
56
|
@typing.overload
|
|
51
57
|
def builder(
|
|
@@ -53,36 +59,32 @@ def builder(
|
|
|
53
59
|
/,
|
|
54
60
|
*,
|
|
55
61
|
gatherer: Callable[[type], dict[str, Field]],
|
|
56
|
-
methods: frozenset[MethodMaker] | set[MethodMaker]
|
|
57
|
-
|
|
62
|
+
methods: frozenset[MethodMaker] | set[MethodMaker],
|
|
63
|
+
flags: dict[str, bool] | None = None,
|
|
64
|
+
) -> Callable[[type[_T]], type[_T]]: ...
|
|
58
65
|
|
|
59
66
|
|
|
60
|
-
_Self = typing.TypeVar("_Self", bound="Field")
|
|
61
|
-
|
|
62
67
|
class Field:
|
|
63
68
|
default: _NothingType | typing.Any
|
|
64
69
|
default_factory: _NothingType | typing.Any
|
|
65
|
-
type: _NothingType |
|
|
70
|
+
type: _NothingType | _py_type
|
|
66
71
|
doc: None | str
|
|
67
72
|
|
|
73
|
+
__classbuilder_internals__: dict
|
|
74
|
+
|
|
68
75
|
def __init__(
|
|
69
76
|
self,
|
|
70
77
|
*,
|
|
71
78
|
default: _NothingType | typing.Any = NOTHING,
|
|
72
79
|
default_factory: _NothingType | typing.Any = NOTHING,
|
|
73
|
-
type: _NothingType |
|
|
80
|
+
type: _NothingType | _py_type = NOTHING,
|
|
74
81
|
doc: None | str = None,
|
|
75
82
|
) -> None: ...
|
|
76
|
-
@property
|
|
77
|
-
def _inherited_slots(self) -> list[str]: ...
|
|
78
83
|
def __repr__(self) -> str: ...
|
|
79
|
-
|
|
80
|
-
def __eq__(self, other: _Self) -> bool: ...
|
|
81
|
-
@typing.overload
|
|
82
|
-
def __eq__(self, other: object) -> NotImplemented: ...
|
|
84
|
+
def __eq__(self, other: Field | object) -> bool: ...
|
|
83
85
|
def validate_field(self) -> None: ...
|
|
84
86
|
@classmethod
|
|
85
|
-
def from_field(cls, fld: Field, **kwargs: typing.Any) ->
|
|
87
|
+
def from_field(cls, fld: Field, /, **kwargs: typing.Any) -> Field: ...
|
|
86
88
|
|
|
87
89
|
|
|
88
90
|
class SlotFields(dict):
|
|
@@ -93,19 +95,20 @@ def slot_gatherer(cls: type) -> dict[str, Field]:
|
|
|
93
95
|
|
|
94
96
|
@typing.overload
|
|
95
97
|
def slotclass(
|
|
96
|
-
cls: type,
|
|
98
|
+
cls: type[_T],
|
|
97
99
|
/,
|
|
98
100
|
*,
|
|
99
101
|
methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
|
|
100
102
|
syntax_check: bool = True
|
|
101
|
-
) ->
|
|
103
|
+
) -> type[_T]: ...
|
|
102
104
|
|
|
105
|
+
@typing.overload
|
|
103
106
|
def slotclass(
|
|
104
107
|
cls: None = None,
|
|
105
108
|
/,
|
|
106
109
|
*,
|
|
107
110
|
methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
|
|
108
111
|
syntax_check: bool = True
|
|
109
|
-
) -> Callable[[type], type]: ...
|
|
112
|
+
) -> Callable[[type[_T]], type[_T]]: ...
|
|
110
113
|
|
|
111
|
-
def fieldclass(cls: type) ->
|
|
114
|
+
def fieldclass(cls: type[_T]) -> type[_T]: ...
|
ducktools/classbuilder/prefab.py
CHANGED
|
@@ -31,7 +31,7 @@ import sys
|
|
|
31
31
|
from . import (
|
|
32
32
|
INTERNALS_DICT, NOTHING,
|
|
33
33
|
Field, MethodMaker, SlotFields,
|
|
34
|
-
builder, fieldclass,
|
|
34
|
+
builder, fieldclass, get_flags, get_fields, slot_gatherer
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
PREFAB_FIELDS = "PREFAB_FIELDS"
|
|
@@ -84,10 +84,11 @@ def get_attributes(cls):
|
|
|
84
84
|
def get_init_maker(*, init_name="__init__"):
|
|
85
85
|
def __init__(cls: "type") -> "tuple[str, dict]":
|
|
86
86
|
globs = {}
|
|
87
|
-
internals = get_internals(cls)
|
|
88
87
|
# Get the internals dictionary and prepare attributes
|
|
89
|
-
attributes =
|
|
90
|
-
|
|
88
|
+
attributes = get_attributes(cls)
|
|
89
|
+
flags = get_flags(cls)
|
|
90
|
+
|
|
91
|
+
kw_only = flags.get("kw_only", False)
|
|
91
92
|
|
|
92
93
|
# Handle pre/post init first - post_init can change types for __init__
|
|
93
94
|
# Get pre and post init arguments
|
|
@@ -319,12 +320,19 @@ def get_eq_maker():
|
|
|
319
320
|
|
|
320
321
|
def get_iter_maker():
|
|
321
322
|
def __iter__(cls: "type") -> "tuple[str, dict]":
|
|
322
|
-
|
|
323
|
+
fields = get_attributes(cls)
|
|
323
324
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
325
|
+
valid_fields = (
|
|
326
|
+
name for name, attrib in fields.items()
|
|
327
|
+
if attrib.iter and not attrib.exclude_field
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
values = "\n".join(f" yield self.{name}" for name in valid_fields)
|
|
331
|
+
|
|
332
|
+
# if values is an empty string
|
|
333
|
+
if not values:
|
|
327
334
|
values = " yield from ()"
|
|
335
|
+
|
|
328
336
|
code = f"def __iter__(self):\n{values}"
|
|
329
337
|
globs = {}
|
|
330
338
|
return code, globs
|
|
@@ -335,14 +343,16 @@ def get_iter_maker():
|
|
|
335
343
|
def get_frozen_setattr_maker():
|
|
336
344
|
def __setattr__(cls: "type") -> "tuple[str, dict]":
|
|
337
345
|
globs = {}
|
|
338
|
-
|
|
339
|
-
|
|
346
|
+
attributes = get_attributes(cls)
|
|
347
|
+
flags = get_flags(cls)
|
|
340
348
|
|
|
341
349
|
# Make the fields set literal
|
|
342
|
-
fields_delimited = ", ".join(f"{field!r}" for field in
|
|
350
|
+
fields_delimited = ", ".join(f"{field!r}" for field in attributes)
|
|
343
351
|
field_set = f"{{ {fields_delimited} }}"
|
|
344
352
|
|
|
345
|
-
|
|
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):
|
|
346
356
|
globs["__prefab_setattr_func"] = object.__setattr__
|
|
347
357
|
setattr_method = "__prefab_setattr_func(self, name, value)"
|
|
348
358
|
else:
|
|
@@ -366,6 +376,7 @@ def get_frozen_setattr_maker():
|
|
|
366
376
|
|
|
367
377
|
|
|
368
378
|
def get_frozen_delattr_maker():
|
|
379
|
+
# noinspection PyUnusedLocal
|
|
369
380
|
def __delattr__(cls: "type") -> "tuple[str, dict]":
|
|
370
381
|
body = (
|
|
371
382
|
' raise TypeError(\n'
|
|
@@ -415,6 +426,7 @@ class Attribute(Field):
|
|
|
415
426
|
init=True,
|
|
416
427
|
repr=True,
|
|
417
428
|
compare=True,
|
|
429
|
+
iter=True,
|
|
418
430
|
kw_only=False,
|
|
419
431
|
in_dict=True,
|
|
420
432
|
exclude_field=False,
|
|
@@ -436,6 +448,7 @@ def attribute(
|
|
|
436
448
|
init=True,
|
|
437
449
|
repr=True,
|
|
438
450
|
compare=True,
|
|
451
|
+
iter=True,
|
|
439
452
|
kw_only=False,
|
|
440
453
|
in_dict=True,
|
|
441
454
|
exclude_field=False,
|
|
@@ -443,8 +456,7 @@ def attribute(
|
|
|
443
456
|
type=NOTHING,
|
|
444
457
|
):
|
|
445
458
|
"""
|
|
446
|
-
|
|
447
|
-
for an instance attribute.
|
|
459
|
+
Get an object to define a prefab Attribute
|
|
448
460
|
|
|
449
461
|
:param default: Default value for this attribute
|
|
450
462
|
:param default_factory: 0 argument callable to give a default value
|
|
@@ -452,6 +464,7 @@ def attribute(
|
|
|
452
464
|
:param init: Include this attribute in the __init__ parameters
|
|
453
465
|
:param repr: Include this attribute in the class __repr__
|
|
454
466
|
:param compare: Include this attribute in the class __eq__
|
|
467
|
+
:param iter: Include this attribute in the class __iter__ if generated
|
|
455
468
|
:param kw_only: Make this argument keyword only in init
|
|
456
469
|
:param in_dict: Include this attribute in methods that serialise to dict
|
|
457
470
|
:param exclude_field: Exclude this field from all magic method generation
|
|
@@ -469,6 +482,7 @@ def attribute(
|
|
|
469
482
|
init=init,
|
|
470
483
|
repr=repr,
|
|
471
484
|
compare=compare,
|
|
485
|
+
iter=iter,
|
|
472
486
|
kw_only=kw_only,
|
|
473
487
|
in_dict=in_dict,
|
|
474
488
|
exclude_field=exclude_field,
|
|
@@ -477,6 +491,14 @@ def attribute(
|
|
|
477
491
|
)
|
|
478
492
|
|
|
479
493
|
|
|
494
|
+
def slot_prefab_gatherer(cls):
|
|
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
|
+
}
|
|
500
|
+
|
|
501
|
+
|
|
480
502
|
# Gatherer for classes built on attributes or annotations
|
|
481
503
|
def attribute_gatherer(cls):
|
|
482
504
|
cls_annotations = cls.__dict__.get("__annotations__", {})
|
|
@@ -588,7 +610,7 @@ def _make_prefab(
|
|
|
588
610
|
|
|
589
611
|
slots = cls_dict.get("__slots__")
|
|
590
612
|
if isinstance(slots, SlotFields):
|
|
591
|
-
gatherer =
|
|
613
|
+
gatherer = slot_prefab_gatherer
|
|
592
614
|
slotted = True
|
|
593
615
|
else:
|
|
594
616
|
gatherer = attribute_gatherer
|
|
@@ -616,18 +638,20 @@ def _make_prefab(
|
|
|
616
638
|
if dict_method:
|
|
617
639
|
methods.add(asdict_desc)
|
|
618
640
|
|
|
641
|
+
flags = {
|
|
642
|
+
"kw_only": kw_only,
|
|
643
|
+
"slotted": slotted,
|
|
644
|
+
}
|
|
645
|
+
|
|
619
646
|
cls = builder(
|
|
620
647
|
cls,
|
|
621
648
|
gatherer=gatherer,
|
|
622
649
|
methods=methods,
|
|
650
|
+
flags=flags,
|
|
623
651
|
)
|
|
624
652
|
|
|
625
|
-
#
|
|
626
|
-
|
|
627
|
-
internals["slotted"] = slotted
|
|
628
|
-
internals["kw_only"] = kw_only
|
|
629
|
-
fields = internals["fields"]
|
|
630
|
-
local_fields = internals["local_fields"]
|
|
653
|
+
# Get fields now the class has been built
|
|
654
|
+
fields = get_fields(cls)
|
|
631
655
|
|
|
632
656
|
# Check pre_init and post_init functions if they exist
|
|
633
657
|
try:
|
|
@@ -699,14 +723,12 @@ def _make_prefab(
|
|
|
699
723
|
if not isinstance(attrib, Attribute):
|
|
700
724
|
attrib = Attribute.from_field(attrib)
|
|
701
725
|
fields[name] = attrib
|
|
702
|
-
if name in local_fields:
|
|
703
|
-
local_fields[name] = attrib
|
|
704
726
|
|
|
705
727
|
# Excluded fields *MUST* be forwarded to post_init
|
|
706
728
|
if attrib.exclude_field:
|
|
707
729
|
if name not in post_init_args:
|
|
708
730
|
raise PrefabError(
|
|
709
|
-
f"{name} is an excluded attribute but is not passed to post_init"
|
|
731
|
+
f"{name!r} is an excluded attribute but is not passed to post_init"
|
|
710
732
|
)
|
|
711
733
|
else:
|
|
712
734
|
valid_args.append(name)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import typing
|
|
2
|
+
from typing_extensions import dataclass_transform
|
|
3
|
+
|
|
2
4
|
from collections.abc import Callable
|
|
3
5
|
|
|
4
6
|
from . import (
|
|
@@ -7,6 +9,9 @@ from . import (
|
|
|
7
9
|
builder, fieldclass, get_internals, slot_gatherer
|
|
8
10
|
)
|
|
9
11
|
|
|
12
|
+
# noinspection PyUnresolvedReferences
|
|
13
|
+
from . import _NothingType
|
|
14
|
+
|
|
10
15
|
PREFAB_FIELDS: str
|
|
11
16
|
PREFAB_INIT_FUNC: str
|
|
12
17
|
PRE_INIT_FUNC: str
|
|
@@ -56,6 +61,7 @@ class Attribute(Field):
|
|
|
56
61
|
init: bool
|
|
57
62
|
repr: bool
|
|
58
63
|
compare: bool
|
|
64
|
+
iter: bool
|
|
59
65
|
kw_only: bool
|
|
60
66
|
in_dict: bool
|
|
61
67
|
exclude_field: bool
|
|
@@ -63,39 +69,40 @@ class Attribute(Field):
|
|
|
63
69
|
def __init__(
|
|
64
70
|
self,
|
|
65
71
|
*,
|
|
66
|
-
default: typing.Any |
|
|
67
|
-
default_factory: typing.Any |
|
|
68
|
-
type: type |
|
|
72
|
+
default: typing.Any | _NothingType = NOTHING,
|
|
73
|
+
default_factory: typing.Any | _NothingType = NOTHING,
|
|
74
|
+
type: type | _NothingType = NOTHING,
|
|
69
75
|
doc: str | None = None,
|
|
70
76
|
init: bool = True,
|
|
71
77
|
repr: bool = True,
|
|
72
78
|
compare: bool = True,
|
|
79
|
+
iter: bool = True,
|
|
73
80
|
kw_only: bool = False,
|
|
74
81
|
in_dict: bool = True,
|
|
75
82
|
exclude_field: bool = False,
|
|
76
83
|
) -> None: ...
|
|
77
84
|
|
|
78
85
|
def __repr__(self) -> str: ...
|
|
79
|
-
|
|
80
|
-
def __eq__(self, other: Attribute) -> bool: ...
|
|
81
|
-
def __eq__(self, other: object) -> NotImplemented: ...
|
|
82
|
-
|
|
86
|
+
def __eq__(self, other: Attribute | object) -> bool: ...
|
|
83
87
|
def validate_field(self) -> None: ...
|
|
84
88
|
|
|
85
89
|
def attribute(
|
|
86
90
|
*,
|
|
87
|
-
default: typing.Any |
|
|
88
|
-
default_factory: typing.Any |
|
|
89
|
-
type: type |
|
|
91
|
+
default: typing.Any | _NothingType = NOTHING,
|
|
92
|
+
default_factory: typing.Any | _NothingType = NOTHING,
|
|
93
|
+
type: type | _NothingType = NOTHING,
|
|
90
94
|
doc: str | None = None,
|
|
91
95
|
init: bool = True,
|
|
92
96
|
repr: bool = True,
|
|
93
97
|
compare: bool = True,
|
|
98
|
+
iter: bool = True,
|
|
94
99
|
kw_only: bool = False,
|
|
95
100
|
in_dict: bool = True,
|
|
96
101
|
exclude_field: bool = False,
|
|
97
102
|
) -> Attribute: ...
|
|
98
103
|
|
|
104
|
+
def slot_prefab_gatherer(cls: type) -> dict[str, Attribute]: ...
|
|
105
|
+
|
|
99
106
|
def attribute_gatherer(cls: type) -> dict[str, Attribute]: ...
|
|
100
107
|
|
|
101
108
|
def _make_prefab(
|
|
@@ -112,9 +119,14 @@ def _make_prefab(
|
|
|
112
119
|
recursive_repr: bool = False,
|
|
113
120
|
) -> type: ...
|
|
114
121
|
|
|
115
|
-
|
|
122
|
+
_T = typing.TypeVar("_T")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# For some reason PyCharm can't see 'attribute'?!?
|
|
126
|
+
# noinspection PyUnresolvedReferences
|
|
127
|
+
@dataclass_transform(field_specifiers=(Attribute, attribute))
|
|
116
128
|
def prefab(
|
|
117
|
-
cls: type | None = None,
|
|
129
|
+
cls: type[_T] | None = None,
|
|
118
130
|
*,
|
|
119
131
|
init: bool = True,
|
|
120
132
|
repr: bool = True,
|
|
@@ -125,7 +137,7 @@ def prefab(
|
|
|
125
137
|
frozen: bool = False,
|
|
126
138
|
dict_method: bool = False,
|
|
127
139
|
recursive_repr: bool = False,
|
|
128
|
-
) -> type | Callable[[type], type]: ...
|
|
140
|
+
) -> type[_T] | Callable[[type[_T]], type[_T]]: ...
|
|
129
141
|
|
|
130
142
|
def build_prefab(
|
|
131
143
|
class_name: str,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ducktools-classbuilder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Toolkit for creating class boilerplate generators
|
|
5
5
|
Author: David C Ellis
|
|
6
6
|
License: MIT License
|
|
@@ -44,6 +44,7 @@ Requires-Dist: sphinx-rtd-theme ; extra == 'docs'
|
|
|
44
44
|
Provides-Extra: testing
|
|
45
45
|
Requires-Dist: pytest ; extra == 'testing'
|
|
46
46
|
Requires-Dist: pytest-cov ; extra == 'testing'
|
|
47
|
+
Requires-Dist: mypy ; extra == 'testing'
|
|
47
48
|
|
|
48
49
|
# Ducktools: Class Builder #
|
|
49
50
|
|
|
@@ -53,19 +54,74 @@ of writing... functions... that will bring back the **joy** of writing classes.
|
|
|
53
54
|
Maybe.
|
|
54
55
|
|
|
55
56
|
While `attrs` and `dataclasses` are class boilerplate generators,
|
|
56
|
-
`ducktools.classbuilder` is intended to be a
|
|
57
|
+
`ducktools.classbuilder` is intended to be a `@dataclass`-like generator.
|
|
57
58
|
The goal is to handle some of the basic functions and to allow for flexible
|
|
58
59
|
customization of both the field collection and the method generation.
|
|
59
60
|
|
|
60
61
|
`ducktools.classbuilder.prefab` includes a prebuilt implementation using these tools.
|
|
61
62
|
|
|
63
|
+
Install from PyPI with:
|
|
64
|
+
`python -m pip install ducktools-classbuilder`
|
|
65
|
+
|
|
66
|
+
## Usage: building a class decorator ##
|
|
67
|
+
|
|
68
|
+
In order to create a class decorator using `ducktools.classbuilder` there are
|
|
69
|
+
a few things you need to prepare.
|
|
70
|
+
|
|
71
|
+
1. A field gathering function to analyse the class and collect valid `Field`s.
|
|
72
|
+
* An example `slot_gatherer` is included.
|
|
73
|
+
2. Code generators that can make use of the gathered `Field`s to create magic method
|
|
74
|
+
source code.
|
|
75
|
+
* Example `init_maker`, `repr_maker` and `eq_maker` generators are included.
|
|
76
|
+
3. A function that calls the `builder` function to apply both of these steps.
|
|
77
|
+
|
|
78
|
+
A field gathering function needs to take the original class as an argument and
|
|
79
|
+
return a dictionary of `{key: Field(...)}` pairs.
|
|
80
|
+
|
|
81
|
+
> [!NOTE]
|
|
82
|
+
> The `builder` will handle inheritance so do not collect fields from parent classes.
|
|
83
|
+
|
|
84
|
+
The code generators take the class as the only argument and return a tuple
|
|
85
|
+
of method source code and globals to be provided to `exec(code, globs)` in order
|
|
86
|
+
to generate the actual method.
|
|
87
|
+
|
|
88
|
+
The provided `slot_gatherer` looks for `__slots__` being assigned a `SlotFields`
|
|
89
|
+
class[^1] where keyword arguments define the names and values for the fields.
|
|
90
|
+
|
|
91
|
+
Code generator functions need to be converted to descriptors before being used.
|
|
92
|
+
This is done using the provided `MethodMaker` descriptor class.
|
|
93
|
+
ex: `init_desc = MethodMaker("__init__", init_maker)`
|
|
94
|
+
|
|
95
|
+
These parts can then be used to make a basic class boilerplate generator by
|
|
96
|
+
providing them to the `builder` function.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from ducktools.classbuilder import (
|
|
100
|
+
builder,
|
|
101
|
+
slot_gatherer,
|
|
102
|
+
init_maker, eq_maker, repr_maker,
|
|
103
|
+
MethodMaker,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
init_desc = MethodMaker("__init__", init_maker)
|
|
107
|
+
repr_desc = MethodMaker("__repr__", repr_maker)
|
|
108
|
+
eq_desc = MethodMaker("__eq__", eq_maker)
|
|
109
|
+
|
|
110
|
+
def slotclass(cls):
|
|
111
|
+
return builder(cls, gatherer=slot_gatherer, methods={init_desc, repr_desc, eq_desc})
|
|
112
|
+
```
|
|
113
|
+
|
|
62
114
|
## Slot Class Usage ##
|
|
63
115
|
|
|
64
|
-
|
|
65
|
-
|
|
116
|
+
This created `slotclass` function can then be used as a decorator to generate classes in
|
|
117
|
+
a similar manner to the `@dataclass` decorator from `dataclasses`.
|
|
118
|
+
|
|
119
|
+
> [!NOTE]
|
|
120
|
+
> `ducktools.classbuilder` includes a premade version of `slotclass` that can
|
|
121
|
+
> be used directly. (The included version has some extra features).
|
|
66
122
|
|
|
67
123
|
```python
|
|
68
|
-
from ducktools.classbuilder import
|
|
124
|
+
from ducktools.classbuilder import Field, SlotFields
|
|
69
125
|
|
|
70
126
|
@slotclass
|
|
71
127
|
class SlottedDC:
|
|
@@ -81,28 +137,36 @@ ex = SlottedDC()
|
|
|
81
137
|
print(ex)
|
|
82
138
|
```
|
|
83
139
|
|
|
84
|
-
|
|
140
|
+
> [!TIP]
|
|
141
|
+
> For more information and examples of creating class generators with additional
|
|
142
|
+
> features using the builder see
|
|
143
|
+
> [the docs](https://ducktools-classbuilder.readthedocs.io/en/latest/extension_examples.html)
|
|
144
|
+
|
|
145
|
+
## Why does your example use `__slots__` instead of annotations? ##
|
|
85
146
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
`__slots__` and decorators work.
|
|
147
|
+
If you want to use `__slots__` in order to save memory you have to declare
|
|
148
|
+
them when the class is originally created as you can't add them later.
|
|
89
149
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
150
|
+
When you use `@dataclass(slots=True)`[^2] with `dataclasses` in order for
|
|
151
|
+
this to work, `dataclasses` has to make a new class and attempt to
|
|
152
|
+
copy over everything from the original.
|
|
153
|
+
This is because decorators operate on classes *after they have been created*
|
|
154
|
+
while slots need to be declared beforehand.
|
|
155
|
+
While you can change the value of `__slots__` after a class has been created,
|
|
156
|
+
this will have no effect on the internal structure of the class.
|
|
96
157
|
|
|
97
158
|
By declaring the class using `__slots__` on the other hand, we can take
|
|
98
159
|
advantage of the fact that it accepts a mapping, where the keys will be
|
|
99
160
|
used as the attributes to create as slots. The values can then be used as
|
|
100
|
-
the default values equivalently to how type hints are used in dataclasses
|
|
161
|
+
the default values equivalently to how type hints are used in `dataclasses`.
|
|
101
162
|
|
|
102
|
-
For example these two classes would be roughly equivalent, except
|
|
163
|
+
For example these two classes would be roughly equivalent, except that
|
|
103
164
|
`@dataclass` has had to recreate the class from scratch while `@slotclass`
|
|
104
|
-
has
|
|
105
|
-
|
|
165
|
+
has added the methods on to the original class.
|
|
166
|
+
This means that any references stored to the original class *before*
|
|
167
|
+
`@dataclass` has rebuilt the class will not be pointing towards the
|
|
168
|
+
correct class.
|
|
169
|
+
This can be demonstrated using a simple class register decorator.
|
|
106
170
|
|
|
107
171
|
> This example requires Python 3.10 as earlier versions of
|
|
108
172
|
> `dataclasses` did not support the `slots` argument.
|
|
@@ -140,7 +204,6 @@ print(SlotCoords())
|
|
|
140
204
|
|
|
141
205
|
print(f"{DataCoords is class_register[DataCoords.__name__] = }")
|
|
142
206
|
print(f"{SlotCoords is class_register[SlotCoords.__name__] = }")
|
|
143
|
-
|
|
144
207
|
```
|
|
145
208
|
|
|
146
209
|
## What features does this have? ##
|
|
@@ -158,9 +221,6 @@ field so they are present on the class if `help(...)` is called.
|
|
|
158
221
|
If you want something with more features you can look at the `prefab.py`
|
|
159
222
|
implementation which provides a 'prebuilt' implementation.
|
|
160
223
|
|
|
161
|
-
For more information on creating class generators using the builder
|
|
162
|
-
see [the docs](https://ducktools-classbuilder.readthedocs.io/en/latest/extension_examples.html)
|
|
163
|
-
|
|
164
224
|
## Will you add \<feature\> to `classbuilder.prefab`? ##
|
|
165
225
|
|
|
166
226
|
No. Not unless it's something I need or find interesting.
|
|
@@ -177,3 +237,9 @@ with a specific feature, you can create or add it yourself.
|
|
|
177
237
|
## Credit ##
|
|
178
238
|
|
|
179
239
|
Heavily inspired by [David Beazley's Cluegen](https://github.com/dabeaz/cluegen)
|
|
240
|
+
|
|
241
|
+
[^1]: `SlotFields` is actually just a subclassed `dict` with no changes. `__slots__`
|
|
242
|
+
works with dictionaries using the values of the keys, while fields are normally
|
|
243
|
+
used for documentation.
|
|
244
|
+
|
|
245
|
+
[^2]: or `@attrs.define`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
ducktools/classbuilder/__init__.py,sha256=OOVk-E6UBi8-68bEy9pqCiKbn9oQ1y4tRGnZ3LGsGYg,13747
|
|
2
|
+
ducktools/classbuilder/__init__.pyi,sha256=-QHJhPn4EjI4XW4OI74MKNg2tGKJiM3w0ELSOvPstAw,2877
|
|
3
|
+
ducktools/classbuilder/prefab.py,sha256=yUa_yoPKU1hzI4klqWJOwTZEXtNOmUU1fQnLuHkVACw,29871
|
|
4
|
+
ducktools/classbuilder/prefab.pyi,sha256=4cyXYqTXtCJv4gPOSNtkBrjORj2jt1bvX6p_sCOzoXw,3963
|
|
5
|
+
ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
|
|
6
|
+
ducktools_classbuilder-0.2.0.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
|
|
7
|
+
ducktools_classbuilder-0.2.0.dist-info/METADATA,sha256=LmHMIhj4jJ9WlmHOac4lBVgW4l6cdNUjG8cEvIULVko,9280
|
|
8
|
+
ducktools_classbuilder-0.2.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
9
|
+
ducktools_classbuilder-0.2.0.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
|
|
10
|
+
ducktools_classbuilder-0.2.0.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
ducktools/classbuilder/__init__.py,sha256=oEdcQdMEqaQjUfbPkHpe55iCiPssDQxf4zxoAMdsTt8,12950
|
|
2
|
-
ducktools/classbuilder/__init__.pyi,sha256=BlpHmxIL4dAiVKgqElRR3wVs7aw73qyJRaSvr-Tuyc0,2770
|
|
3
|
-
ducktools/classbuilder/prefab.py,sha256=qQJzN4ys6Av6s9NaabzSqRKE5BXDfhidt3EtGZ0HAxQ,29405
|
|
4
|
-
ducktools/classbuilder/prefab.pyi,sha256=wrq8NKwy9TsQ6fpnMyTf4DaGtCOK_90NHeU61CfNOo4,3589
|
|
5
|
-
ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
|
|
6
|
-
ducktools_classbuilder-0.1.0.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
|
|
7
|
-
ducktools_classbuilder-0.1.0.dist-info/METADATA,sha256=FHgeLZx-RfcvY2ty1l6O19z449So-UQZIcBinJ8n5xU,6675
|
|
8
|
-
ducktools_classbuilder-0.1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
9
|
-
ducktools_classbuilder-0.1.0.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
|
|
10
|
-
ducktools_classbuilder-0.1.0.dist-info/RECORD,,
|
{ducktools_classbuilder-0.1.0.dist-info → ducktools_classbuilder-0.2.0.dist-info}/LICENSE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.1.0.dist-info → ducktools_classbuilder-0.2.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|