ducktools-classbuilder 0.9.0__py3-none-any.whl → 0.10.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 +191 -67
- ducktools/classbuilder/__init__.pyi +18 -9
- ducktools/classbuilder/_version.py +2 -2
- ducktools/classbuilder/annotations.py +2 -13
- ducktools/classbuilder/prefab.py +9 -3
- ducktools/classbuilder/prefab.pyi +3 -0
- {ducktools_classbuilder-0.9.0.dist-info → ducktools_classbuilder-0.10.0.dist-info}/METADATA +8 -17
- ducktools_classbuilder-0.10.0.dist-info/RECORD +13 -0
- {ducktools_classbuilder-0.9.0.dist-info → ducktools_classbuilder-0.10.0.dist-info}/WHEEL +1 -1
- ducktools_classbuilder-0.9.0.dist-info/RECORD +0 -13
- {ducktools_classbuilder-0.9.0.dist-info → ducktools_classbuilder-0.10.0.dist-info/licenses}/LICENSE +0 -0
- {ducktools_classbuilder-0.9.0.dist-info → ducktools_classbuilder-0.10.0.dist-info}/top_level.txt +0 -0
|
@@ -38,7 +38,7 @@ from ._version import __version__, __version_tuple__ # noqa: F401
|
|
|
38
38
|
# Change this name if you make heavy modifications
|
|
39
39
|
INTERNALS_DICT = "__classbuilder_internals__"
|
|
40
40
|
META_GATHERER_NAME = "_meta_gatherer"
|
|
41
|
-
|
|
41
|
+
GATHERED_DATA = "__classbuilder_gathered_fields__"
|
|
42
42
|
|
|
43
43
|
# If testing, make Field classes frozen to make sure attributes are not
|
|
44
44
|
# overwritten. When running this is a performance penalty so it is not required.
|
|
@@ -114,12 +114,16 @@ FIELD_NOTHING = _NothingType("FIELD")
|
|
|
114
114
|
# KW_ONLY sentinel 'type' to use to indicate all subsequent attributes are
|
|
115
115
|
# keyword only
|
|
116
116
|
# noinspection PyPep8Naming
|
|
117
|
-
class
|
|
117
|
+
class _KW_ONLY_META(type):
|
|
118
118
|
def __repr__(self):
|
|
119
|
-
return "<KW_ONLY Sentinel
|
|
119
|
+
return "<KW_ONLY Sentinel>"
|
|
120
120
|
|
|
121
121
|
|
|
122
|
-
KW_ONLY
|
|
122
|
+
class KW_ONLY(metaclass=_KW_ONLY_META):
|
|
123
|
+
"""
|
|
124
|
+
Sentinel Class to indicate that variables declared after
|
|
125
|
+
this sentinel are to be converted to KW_ONLY arguments.
|
|
126
|
+
"""
|
|
123
127
|
|
|
124
128
|
|
|
125
129
|
class GeneratedCode:
|
|
@@ -385,6 +389,36 @@ def eq_generator(cls, funcname="__eq__"):
|
|
|
385
389
|
return GeneratedCode(code, globs)
|
|
386
390
|
|
|
387
391
|
|
|
392
|
+
def replace_generator(cls, funcname="__replace__"):
|
|
393
|
+
# Generate the replace method for built classes
|
|
394
|
+
# unlike the dataclasses implementation this is generated
|
|
395
|
+
attribs = get_fields(cls)
|
|
396
|
+
|
|
397
|
+
# This is essentially the as_dict generator for prefabs
|
|
398
|
+
# except based on attrib.init instead of .serialize
|
|
399
|
+
vals = ", ".join(
|
|
400
|
+
f"'{name}': self.{name}"
|
|
401
|
+
for name, attrib in attribs.items()
|
|
402
|
+
if attrib.init
|
|
403
|
+
)
|
|
404
|
+
init_dict = f"{{{vals}}}"
|
|
405
|
+
|
|
406
|
+
code = (
|
|
407
|
+
f"def {funcname}(self, /, **changes):\n"
|
|
408
|
+
f" new_kwargs = {init_dict}\n"
|
|
409
|
+
f" for name, value in changes.items():\n"
|
|
410
|
+
f" if name not in new_kwargs:\n"
|
|
411
|
+
f" raise TypeError(\n"
|
|
412
|
+
f" f\"{{name!r}} is not a valid replacable \"\n"
|
|
413
|
+
f" f\"field on {{self.__class__.__name__!r}}\"\n"
|
|
414
|
+
f" )\n"
|
|
415
|
+
f" new_kwargs[name] = value\n"
|
|
416
|
+
f" return self.__class__(**new_kwargs)\n"
|
|
417
|
+
)
|
|
418
|
+
globs = {}
|
|
419
|
+
return GeneratedCode(code, globs)
|
|
420
|
+
|
|
421
|
+
|
|
388
422
|
def frozen_setattr_generator(cls, funcname="__setattr__"):
|
|
389
423
|
globs = {}
|
|
390
424
|
field_names = set(get_fields(cls))
|
|
@@ -433,6 +467,7 @@ def frozen_delattr_generator(cls, funcname="__delattr__"):
|
|
|
433
467
|
init_maker = MethodMaker("__init__", init_generator)
|
|
434
468
|
repr_maker = MethodMaker("__repr__", repr_generator)
|
|
435
469
|
eq_maker = MethodMaker("__eq__", eq_generator)
|
|
470
|
+
replace_maker = MethodMaker("__replace__", replace_generator)
|
|
436
471
|
frozen_setattr_maker = MethodMaker("__setattr__", frozen_setattr_generator)
|
|
437
472
|
frozen_delattr_maker = MethodMaker("__delattr__", frozen_delattr_generator)
|
|
438
473
|
default_methods = frozenset({init_maker, repr_maker, eq_maker})
|
|
@@ -540,21 +575,25 @@ class SlotMakerMeta(type):
|
|
|
540
575
|
|
|
541
576
|
Will not convert `ClassVar` hinted values.
|
|
542
577
|
"""
|
|
543
|
-
def __new__(cls, name, bases, ns, slots=True, **kwargs):
|
|
578
|
+
def __new__(cls, name, bases, ns, slots=True, gatherer=None, **kwargs):
|
|
544
579
|
# This should only run if slots=True is declared
|
|
545
580
|
# and __slots__ have not already been defined
|
|
546
|
-
if slots and "__slots__" not in ns:
|
|
581
|
+
if slots and "__slots__" not in ns:
|
|
547
582
|
# Check if a different gatherer has been set in any base classes
|
|
548
583
|
# Default to unified gatherer
|
|
549
|
-
gatherer
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
584
|
+
if gatherer is None:
|
|
585
|
+
gatherer = ns.get(META_GATHERER_NAME, None)
|
|
586
|
+
if not gatherer:
|
|
587
|
+
for base in bases:
|
|
588
|
+
if g := getattr(base, META_GATHERER_NAME, None):
|
|
589
|
+
gatherer = g
|
|
590
|
+
break
|
|
591
|
+
|
|
592
|
+
if not gatherer:
|
|
593
|
+
gatherer = unified_gatherer
|
|
555
594
|
|
|
556
|
-
|
|
557
|
-
|
|
595
|
+
# Set the gatherer in the namespace
|
|
596
|
+
ns[META_GATHERER_NAME] = gatherer
|
|
558
597
|
|
|
559
598
|
# Obtain slots from annotations or attributes
|
|
560
599
|
cls_fields, cls_modifications = gatherer(ns)
|
|
@@ -564,14 +603,54 @@ class SlotMakerMeta(type):
|
|
|
564
603
|
else:
|
|
565
604
|
ns[k] = v
|
|
566
605
|
|
|
606
|
+
slots = {}
|
|
607
|
+
fields = {}
|
|
608
|
+
|
|
609
|
+
for k, v in cls_fields.items():
|
|
610
|
+
slots[k] = v.doc
|
|
611
|
+
if k not in {"__weakref__", "__dict__"}:
|
|
612
|
+
fields[k] = v
|
|
613
|
+
|
|
567
614
|
# Place slots *after* everything else to be safe
|
|
568
|
-
ns["__slots__"] =
|
|
615
|
+
ns["__slots__"] = slots
|
|
616
|
+
|
|
617
|
+
# Place pre-gathered field data - modifications are already applied
|
|
618
|
+
modifications = {}
|
|
619
|
+
ns[GATHERED_DATA] = fields, modifications
|
|
620
|
+
|
|
621
|
+
else:
|
|
622
|
+
if gatherer is not None:
|
|
623
|
+
ns[META_GATHERER_NAME] = gatherer
|
|
569
624
|
|
|
570
625
|
new_cls = super().__new__(cls, name, bases, ns, **kwargs)
|
|
571
626
|
|
|
572
627
|
return new_cls
|
|
573
628
|
|
|
574
629
|
|
|
630
|
+
# This class is set up before fields as it will be used to generate the Fields
|
|
631
|
+
# for Field itself so Field can have generated __eq__, __repr__ and other methods
|
|
632
|
+
class GatheredFields:
|
|
633
|
+
"""
|
|
634
|
+
Helper class to store gathered field data
|
|
635
|
+
"""
|
|
636
|
+
__slots__ = ("fields", "modifications")
|
|
637
|
+
|
|
638
|
+
def __init__(self, fields, modifications):
|
|
639
|
+
self.fields = fields
|
|
640
|
+
self.modifications = modifications
|
|
641
|
+
|
|
642
|
+
def __eq__(self, other):
|
|
643
|
+
if type(self) is type(other):
|
|
644
|
+
return self.fields == other.fields and self.modifications == other.modifications
|
|
645
|
+
|
|
646
|
+
def __repr__(self):
|
|
647
|
+
return f"{type(self).__name__}(fields={self.fields!r}, modifications={self.modifications!r})"
|
|
648
|
+
|
|
649
|
+
def __call__(self, cls_dict):
|
|
650
|
+
# cls_dict will be provided, but isn't needed
|
|
651
|
+
return self.fields, self.modifications
|
|
652
|
+
|
|
653
|
+
|
|
575
654
|
# The Field class can finally be defined.
|
|
576
655
|
# The __init__ method has to be written manually so Fields can be created
|
|
577
656
|
# However after this, the other methods can be generated.
|
|
@@ -597,17 +676,18 @@ class Field(metaclass=SlotMakerMeta):
|
|
|
597
676
|
:param compare: Include in the class __eq__.
|
|
598
677
|
:param kw_only: Make this a keyword only parameter in __init__.
|
|
599
678
|
"""
|
|
600
|
-
|
|
601
|
-
#
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
679
|
+
|
|
680
|
+
# Plain slots are required as part of bootstrapping
|
|
681
|
+
# This prevents SlotMakerMeta from trying to generate 'Field's
|
|
682
|
+
__slots__ = (
|
|
683
|
+
"default",
|
|
684
|
+
"default_factory",
|
|
685
|
+
"type",
|
|
686
|
+
"doc",
|
|
687
|
+
"init",
|
|
688
|
+
"repr",
|
|
689
|
+
"compare",
|
|
690
|
+
"kw_only",
|
|
611
691
|
)
|
|
612
692
|
|
|
613
693
|
# noinspection PyShadowingBuiltins
|
|
@@ -641,6 +721,7 @@ class Field(metaclass=SlotMakerMeta):
|
|
|
641
721
|
self.validate_field()
|
|
642
722
|
|
|
643
723
|
def __init_subclass__(cls, frozen=False):
|
|
724
|
+
# Subclasses of Field can be created as if they are dataclasses
|
|
644
725
|
field_methods = {_field_init_maker, repr_maker, eq_maker}
|
|
645
726
|
if frozen or _UNDER_TESTING:
|
|
646
727
|
field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
@@ -676,6 +757,66 @@ class Field(metaclass=SlotMakerMeta):
|
|
|
676
757
|
return cls(**argument_dict)
|
|
677
758
|
|
|
678
759
|
|
|
760
|
+
def _build_field():
|
|
761
|
+
# Complete the construction of the Field class
|
|
762
|
+
field_docs = {
|
|
763
|
+
"default": "Standard default value to be used for attributes with this field.",
|
|
764
|
+
"default_factory":
|
|
765
|
+
"A zero-argument function to be called to generate a default value, "
|
|
766
|
+
"useful for mutable obects like lists.",
|
|
767
|
+
"type": "The type of the attribute to be assigned by this field.",
|
|
768
|
+
"doc":
|
|
769
|
+
"The documentation for the attribute that appears when calling "
|
|
770
|
+
"help(...) on the class. (Only in slotted classes).",
|
|
771
|
+
"init": "Include this attribute in the class __init__ parameters.",
|
|
772
|
+
"repr": "Include this attribute in the class __repr__",
|
|
773
|
+
"compare": "Include this attribute in the class __eq__ method",
|
|
774
|
+
"kw_only": "Make this a keyword only parameter in __init__",
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
fields = {
|
|
778
|
+
"default": Field(default=NOTHING, doc=field_docs["default"]),
|
|
779
|
+
"default_factory": Field(default=NOTHING, doc=field_docs["default_factory"]),
|
|
780
|
+
"type": Field(default=NOTHING, doc=field_docs["type"]),
|
|
781
|
+
"doc": Field(default=None, doc=field_docs["doc"]),
|
|
782
|
+
"init": Field(default=True, doc=field_docs["init"]),
|
|
783
|
+
"repr": Field(default=True, doc=field_docs["repr"]),
|
|
784
|
+
"compare": Field(default=True, doc=field_docs["compare"]),
|
|
785
|
+
"kw_only": Field(default=False, doc=field_docs["kw_only"])
|
|
786
|
+
}
|
|
787
|
+
modifications = {"__slots__": field_docs}
|
|
788
|
+
|
|
789
|
+
field_methods = {repr_maker, eq_maker}
|
|
790
|
+
if _UNDER_TESTING:
|
|
791
|
+
field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
792
|
+
|
|
793
|
+
builder(
|
|
794
|
+
Field,
|
|
795
|
+
gatherer=GatheredFields(fields, modifications),
|
|
796
|
+
methods=field_methods,
|
|
797
|
+
flags={"slotted": True, "kw_only": True},
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
_build_field()
|
|
802
|
+
del _build_field
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def pre_gathered_gatherer(cls_or_ns):
|
|
806
|
+
"""
|
|
807
|
+
Retrieve fields previously gathered by SlotMakerMeta
|
|
808
|
+
|
|
809
|
+
:param cls_or_ns: Class to gather field information from (or class namespace)
|
|
810
|
+
:return: dict of field_name: Field(...) and modifications to be performed by the builder
|
|
811
|
+
"""
|
|
812
|
+
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
813
|
+
cls_dict = cls_or_ns
|
|
814
|
+
else:
|
|
815
|
+
cls_dict = cls_or_ns.__dict__
|
|
816
|
+
|
|
817
|
+
return cls_dict[GATHERED_DATA]
|
|
818
|
+
|
|
819
|
+
|
|
679
820
|
def make_slot_gatherer(field_type=Field):
|
|
680
821
|
"""
|
|
681
822
|
Create a new annotation gatherer that will work with `Field` instances
|
|
@@ -690,7 +831,7 @@ def make_slot_gatherer(field_type=Field):
|
|
|
690
831
|
Gather field information for class generation based on __slots__
|
|
691
832
|
|
|
692
833
|
:param cls_or_ns: Class to gather field information from (or class namespace)
|
|
693
|
-
:return: dict of field_name: Field(...)
|
|
834
|
+
:return: dict of field_name: Field(...) and modifications to be performed by the builder
|
|
694
835
|
"""
|
|
695
836
|
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
696
837
|
cls_dict = cls_or_ns
|
|
@@ -852,6 +993,7 @@ def make_field_gatherer(
|
|
|
852
993
|
def make_unified_gatherer(
|
|
853
994
|
field_type=Field,
|
|
854
995
|
leave_default_values=False,
|
|
996
|
+
ignore_annotations=False,
|
|
855
997
|
):
|
|
856
998
|
"""
|
|
857
999
|
Create a gatherer that will work via first slots, then
|
|
@@ -860,6 +1002,7 @@ def make_unified_gatherer(
|
|
|
860
1002
|
|
|
861
1003
|
:param field_type: The field class to use for gathering
|
|
862
1004
|
:param leave_default_values: leave default values in place
|
|
1005
|
+
:param ignore_annotations: don't attempt to read annotations
|
|
863
1006
|
:return: gatherer function
|
|
864
1007
|
"""
|
|
865
1008
|
slot_g = make_slot_gatherer(field_type)
|
|
@@ -872,27 +1015,35 @@ def make_unified_gatherer(
|
|
|
872
1015
|
else:
|
|
873
1016
|
cls_dict = cls_or_ns.__dict__
|
|
874
1017
|
|
|
1018
|
+
cls_gathered = cls_dict.get(GATHERED_DATA)
|
|
1019
|
+
if cls_gathered:
|
|
1020
|
+
return pre_gathered_gatherer(cls_dict)
|
|
1021
|
+
|
|
875
1022
|
cls_slots = cls_dict.get("__slots__")
|
|
876
1023
|
|
|
877
1024
|
if isinstance(cls_slots, SlotFields):
|
|
878
1025
|
return slot_g(cls_dict)
|
|
879
1026
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1027
|
+
if ignore_annotations:
|
|
1028
|
+
return attrib_g(cls_dict)
|
|
1029
|
+
else:
|
|
1030
|
+
# To choose between annotation and attribute gatherers
|
|
1031
|
+
# compare sets of names.
|
|
1032
|
+
# Don't bother evaluating string annotations, as we only need names
|
|
1033
|
+
cls_annotations = get_ns_annotations(cls_dict)
|
|
1034
|
+
cls_attributes = {
|
|
1035
|
+
k: v for k, v in cls_dict.items() if isinstance(v, field_type)
|
|
1036
|
+
}
|
|
887
1037
|
|
|
888
|
-
|
|
889
|
-
|
|
1038
|
+
cls_annotation_names = cls_annotations.keys()
|
|
1039
|
+
cls_attribute_names = cls_attributes.keys()
|
|
890
1040
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1041
|
+
if set(cls_annotation_names).issuperset(set(cls_attribute_names)):
|
|
1042
|
+
# All `Field` values have annotations, so use annotation gatherer
|
|
1043
|
+
return anno_g(cls_dict)
|
|
894
1044
|
|
|
895
|
-
|
|
1045
|
+
return attrib_g(cls_dict)
|
|
1046
|
+
|
|
896
1047
|
return field_unified_gatherer
|
|
897
1048
|
|
|
898
1049
|
|
|
@@ -904,19 +1055,6 @@ annotation_gatherer = make_annotation_gatherer()
|
|
|
904
1055
|
unified_gatherer = make_unified_gatherer()
|
|
905
1056
|
|
|
906
1057
|
|
|
907
|
-
# Now the gatherers have been defined, add __repr__ and __eq__ to Field.
|
|
908
|
-
_field_methods = {repr_maker, eq_maker}
|
|
909
|
-
if _UNDER_TESTING:
|
|
910
|
-
_field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
911
|
-
|
|
912
|
-
builder(
|
|
913
|
-
Field,
|
|
914
|
-
gatherer=slot_gatherer,
|
|
915
|
-
methods=_field_methods,
|
|
916
|
-
flags={"slotted": True, "kw_only": True},
|
|
917
|
-
)
|
|
918
|
-
|
|
919
|
-
|
|
920
1058
|
def check_argument_order(cls):
|
|
921
1059
|
"""
|
|
922
1060
|
Raise a SyntaxError if the argument order will be invalid for a generated
|
|
@@ -959,17 +1097,3 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
|
|
|
959
1097
|
check_argument_order(cls)
|
|
960
1098
|
|
|
961
1099
|
return cls
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
@slotclass
|
|
965
|
-
class GatheredFields:
|
|
966
|
-
"""
|
|
967
|
-
A helper gatherer for fields that have been gathered externally.
|
|
968
|
-
"""
|
|
969
|
-
__slots__ = SlotFields(
|
|
970
|
-
fields=Field(),
|
|
971
|
-
modifications=Field(),
|
|
972
|
-
)
|
|
973
|
-
|
|
974
|
-
def __call__(self, cls):
|
|
975
|
-
return self.fields, self.modifications
|
|
@@ -13,6 +13,7 @@ __version__: str
|
|
|
13
13
|
__version_tuple__: tuple[str | int, ...]
|
|
14
14
|
INTERNALS_DICT: str
|
|
15
15
|
META_GATHERER_NAME: str
|
|
16
|
+
GATHERED_DATA: str
|
|
16
17
|
|
|
17
18
|
def get_fields(cls: type, *, local: bool = False) -> dict[str, Field]: ...
|
|
18
19
|
|
|
@@ -28,11 +29,11 @@ class _NothingType:
|
|
|
28
29
|
NOTHING: _NothingType
|
|
29
30
|
FIELD_NOTHING: _NothingType
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
class _KW_ONLY_TYPE:
|
|
32
|
+
class _KW_ONLY_META(type):
|
|
33
33
|
def __repr__(self) -> str: ...
|
|
34
34
|
|
|
35
|
-
KW_ONLY:
|
|
35
|
+
class KW_ONLY(metaclass=_KW_ONLY_META): ...
|
|
36
|
+
|
|
36
37
|
# Stub Only
|
|
37
38
|
@typing.type_check_only
|
|
38
39
|
class _CodegenType(typing.Protocol):
|
|
@@ -73,6 +74,7 @@ def get_repr_generator(
|
|
|
73
74
|
) -> _CodegenType: ...
|
|
74
75
|
def repr_generator(cls: type, funcname: str = "__repr__") -> GeneratedCode: ...
|
|
75
76
|
def eq_generator(cls: type, funcname: str = "__eq__") -> GeneratedCode: ...
|
|
77
|
+
def replace_generator(cls: type, funcname: str = "__replace__") -> GeneratedCode: ...
|
|
76
78
|
|
|
77
79
|
def frozen_setattr_generator(cls: type, funcname: str = "__setattr__") -> GeneratedCode: ...
|
|
78
80
|
|
|
@@ -81,6 +83,7 @@ def frozen_delattr_generator(cls: type, funcname: str = "__delattr__") -> Genera
|
|
|
81
83
|
init_maker: MethodMaker
|
|
82
84
|
repr_maker: MethodMaker
|
|
83
85
|
eq_maker: MethodMaker
|
|
86
|
+
replace_maker: MethodMaker
|
|
84
87
|
frozen_setattr_maker: MethodMaker
|
|
85
88
|
frozen_delattr_maker: MethodMaker
|
|
86
89
|
default_methods: frozenset[MethodMaker]
|
|
@@ -120,7 +123,8 @@ class SlotMakerMeta(type):
|
|
|
120
123
|
name: str,
|
|
121
124
|
bases: tuple[type, ...],
|
|
122
125
|
ns: dict[str, typing.Any],
|
|
123
|
-
slots: bool =
|
|
126
|
+
slots: bool = ...,
|
|
127
|
+
gatherer: Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]] | None = ...,
|
|
124
128
|
**kwargs: typing.Any,
|
|
125
129
|
) -> _T: ...
|
|
126
130
|
|
|
@@ -165,6 +169,9 @@ class Field(metaclass=SlotMakerMeta):
|
|
|
165
169
|
_ReturnsField = Callable[..., Field]
|
|
166
170
|
_FieldType = typing.TypeVar("_FieldType", bound=Field)
|
|
167
171
|
|
|
172
|
+
def pre_gathered_gatherer(
|
|
173
|
+
cls_or_ns: type | _CopiableMappings
|
|
174
|
+
) -> tuple[dict[str, Field | _FieldType], dict[str, typing.Any]]: ...
|
|
168
175
|
|
|
169
176
|
@typing.overload
|
|
170
177
|
def make_slot_gatherer(
|
|
@@ -204,13 +211,15 @@ def make_field_gatherer(
|
|
|
204
211
|
@typing.overload
|
|
205
212
|
def make_unified_gatherer(
|
|
206
213
|
field_type: type[_FieldType],
|
|
207
|
-
leave_default_values: bool =
|
|
214
|
+
leave_default_values: bool = ...,
|
|
215
|
+
ignore_annotations: bool = ...,
|
|
208
216
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
|
|
209
217
|
|
|
210
218
|
@typing.overload
|
|
211
219
|
def make_unified_gatherer(
|
|
212
|
-
field_type: _ReturnsField =
|
|
213
|
-
leave_default_values: bool =
|
|
220
|
+
field_type: _ReturnsField = ...,
|
|
221
|
+
leave_default_values: bool = ...,
|
|
222
|
+
ignore_annotations: bool = ...,
|
|
214
223
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
|
|
215
224
|
|
|
216
225
|
|
|
@@ -244,7 +253,7 @@ def slotclass(
|
|
|
244
253
|
_gatherer_type = Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]
|
|
245
254
|
|
|
246
255
|
class GatheredFields:
|
|
247
|
-
__slots__:
|
|
256
|
+
__slots__: tuple[str, ...]
|
|
248
257
|
|
|
249
258
|
fields: dict[str, Field]
|
|
250
259
|
modifications: dict[str, typing.Any]
|
|
@@ -260,4 +269,4 @@ class GatheredFields:
|
|
|
260
269
|
|
|
261
270
|
def __repr__(self) -> str: ...
|
|
262
271
|
def __eq__(self, other) -> bool: ...
|
|
263
|
-
def __call__(self,
|
|
272
|
+
def __call__(self, cls_dict: type | dict[str, typing.Any]) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "0.
|
|
2
|
-
__version_tuple__ = (0,
|
|
1
|
+
__version__ = "0.10.0"
|
|
2
|
+
__version_tuple__ = (0, 10, 0)
|
|
@@ -70,19 +70,8 @@ def is_classvar(hint):
|
|
|
70
70
|
else:
|
|
71
71
|
_typing = sys.modules.get("typing")
|
|
72
72
|
if _typing:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
# 3.8 also needs get_origin from typing_extensions
|
|
76
|
-
if sys.version_info < (3, 10):
|
|
77
|
-
_typing_extensions = sys.modules.get("typing_extensions")
|
|
78
|
-
if _typing_extensions:
|
|
79
|
-
_Annotated = _typing_extensions.Annotated
|
|
80
|
-
_get_origin = _typing_extensions.get_origin
|
|
81
|
-
else:
|
|
82
|
-
_Annotated, _get_origin = None, None
|
|
83
|
-
else:
|
|
84
|
-
_Annotated = _typing.Annotated
|
|
85
|
-
_get_origin = _typing.get_origin
|
|
73
|
+
_Annotated = _typing.Annotated
|
|
74
|
+
_get_origin = _typing.get_origin
|
|
86
75
|
|
|
87
76
|
if _Annotated and _get_origin(hint) is _Annotated:
|
|
88
77
|
hint = getattr(hint, "__origin__", None)
|
ducktools/classbuilder/prefab.py
CHANGED
|
@@ -30,7 +30,7 @@ from . import (
|
|
|
30
30
|
Field, MethodMaker, GatheredFields, GeneratedCode, SlotMakerMeta,
|
|
31
31
|
builder, get_flags, get_fields,
|
|
32
32
|
make_unified_gatherer,
|
|
33
|
-
frozen_setattr_maker, frozen_delattr_maker,
|
|
33
|
+
eq_maker, frozen_setattr_maker, frozen_delattr_maker, replace_maker,
|
|
34
34
|
get_repr_generator,
|
|
35
35
|
)
|
|
36
36
|
|
|
@@ -441,6 +441,8 @@ def _make_prefab(
|
|
|
441
441
|
if dict_method:
|
|
442
442
|
methods.add(asdict_maker)
|
|
443
443
|
|
|
444
|
+
methods.add(replace_maker)
|
|
445
|
+
|
|
444
446
|
flags = {
|
|
445
447
|
"kw_only": kw_only,
|
|
446
448
|
"slotted": slotted,
|
|
@@ -549,8 +551,7 @@ def _make_prefab(
|
|
|
549
551
|
return cls
|
|
550
552
|
|
|
551
553
|
|
|
552
|
-
class Prefab(metaclass=SlotMakerMeta):
|
|
553
|
-
_meta_gatherer = prefab_gatherer
|
|
554
|
+
class Prefab(metaclass=SlotMakerMeta, gatherer=prefab_gatherer):
|
|
554
555
|
__slots__ = {} # type: ignore
|
|
555
556
|
|
|
556
557
|
# noinspection PyShadowingBuiltins
|
|
@@ -779,3 +780,8 @@ def as_dict(o):
|
|
|
779
780
|
for name, attrib in flds.items()
|
|
780
781
|
if attrib.serialize
|
|
781
782
|
}
|
|
783
|
+
|
|
784
|
+
def replace(obj, /, **changes):
|
|
785
|
+
if not is_prefab_instance(obj):
|
|
786
|
+
raise TypeError("replace() should be called on prefab instances")
|
|
787
|
+
return obj.__replace__(**changes)
|
|
@@ -48,6 +48,7 @@ hash_maker: MethodMaker
|
|
|
48
48
|
class Attribute(Field):
|
|
49
49
|
__slots__: dict
|
|
50
50
|
__signature__: inspect.Signature
|
|
51
|
+
__classbuilder_gathered_fields__: tuple[dict[str, Field], dict[str, typing.Any]]
|
|
51
52
|
|
|
52
53
|
iter: bool
|
|
53
54
|
serialize: bool
|
|
@@ -238,3 +239,5 @@ def is_prefab(o: typing.Any) -> bool: ...
|
|
|
238
239
|
def is_prefab_instance(o: object) -> bool: ...
|
|
239
240
|
|
|
240
241
|
def as_dict(o) -> dict[str, typing.Any]: ...
|
|
242
|
+
|
|
243
|
+
def replace(obj: _T, /, **changes: typing.Any) -> _T: ...
|
|
@@ -1,34 +1,25 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: ducktools-classbuilder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Toolkit for creating class boilerplate generators
|
|
5
5
|
Author: David C Ellis
|
|
6
6
|
Project-URL: Homepage, https://github.com/davidcellis/ducktools-classbuilder
|
|
7
7
|
Classifier: Development Status :: 4 - Beta
|
|
8
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
10
8
|
Classifier: Programming Language :: Python :: 3.10
|
|
11
9
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
11
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
13
|
Classifier: Operating System :: OS Independent
|
|
15
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
-
Requires-Python: >=3.
|
|
15
|
+
Requires-Python: >=3.10
|
|
17
16
|
Description-Content-Type: text/markdown
|
|
18
17
|
License-File: LICENSE
|
|
19
18
|
Provides-Extra: docs
|
|
20
|
-
Requires-Dist: sphinx
|
|
21
|
-
Requires-Dist: myst-parser
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
|
|
24
|
-
Requires-Dist: attrs ; extra == 'performance_tests'
|
|
25
|
-
Requires-Dist: pydantic ; extra == 'performance_tests'
|
|
26
|
-
Provides-Extra: testing
|
|
27
|
-
Requires-Dist: pytest >=8.2 ; extra == 'testing'
|
|
28
|
-
Requires-Dist: pytest-cov ; extra == 'testing'
|
|
29
|
-
Requires-Dist: typing-extensions ; extra == 'testing'
|
|
30
|
-
Provides-Extra: type_checking
|
|
31
|
-
Requires-Dist: mypy ; extra == 'type_checking'
|
|
19
|
+
Requires-Dist: sphinx>=8.1; extra == "docs"
|
|
20
|
+
Requires-Dist: myst-parser>=4.0; extra == "docs"
|
|
21
|
+
Requires-Dist: sphinx_rtd_theme>=3.0; extra == "docs"
|
|
22
|
+
Dynamic: license-file
|
|
32
23
|
|
|
33
24
|
# Ducktools: Class Builder #
|
|
34
25
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
ducktools/classbuilder/__init__.py,sha256=nZz_pQM6CN7yCT71oOclih4uls73a6SlW-HY2gxzYIs,37244
|
|
2
|
+
ducktools/classbuilder/__init__.pyi,sha256=v9QDO8A0AdPlAPNOXZGYXu1FaAq3yelwWNucdfWudrs,8143
|
|
3
|
+
ducktools/classbuilder/_version.py,sha256=MdjuHfU3W8hJQpD3kGrPvowqtRDz5JMifDCCjNbrskE,54
|
|
4
|
+
ducktools/classbuilder/annotations.py,sha256=GgBvNthDSRvKKZ9R_qxEVtCV3_vGuEBfuqQJFcDAe1s,3005
|
|
5
|
+
ducktools/classbuilder/annotations.pyi,sha256=c5vYtULdDgMYWtkzeYMsHIbmnEuT2Ru-nNZieWvYuQ4,247
|
|
6
|
+
ducktools/classbuilder/prefab.py,sha256=2GldQTRvOcFzB68qXbymeHKiXoyHx7eOjGxeilFIcn0,24632
|
|
7
|
+
ducktools/classbuilder/prefab.pyi,sha256=q5Zca5wAN80GE4uICY6v3EIB6LvIcjOEhzD1pVDK44U,6507
|
|
8
|
+
ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
|
|
9
|
+
ducktools_classbuilder-0.10.0.dist-info/licenses/LICENSE,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
|
|
10
|
+
ducktools_classbuilder-0.10.0.dist-info/METADATA,sha256=zrp9Q-klxkuTdsGSGz87rEZ7nxM8FCEppJfiGwqRh-0,9231
|
|
11
|
+
ducktools_classbuilder-0.10.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
ducktools_classbuilder-0.10.0.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
|
|
13
|
+
ducktools_classbuilder-0.10.0.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
ducktools/classbuilder/__init__.py,sha256=IfMN45iuJJ0BadtmjwjoYhXCLw6WOSQTMk9s_cxGT0M,32312
|
|
2
|
-
ducktools/classbuilder/__init__.pyi,sha256=66VOHYndP7z83Bl7QzTOyoF8j2nLFRRFfaQ6-mTdnmo,7683
|
|
3
|
-
ducktools/classbuilder/_version.py,sha256=RJaQx44-8b_pifglxqeP0c25pHPZ-Zu98duqjj_ZSOg,52
|
|
4
|
-
ducktools/classbuilder/annotations.py,sha256=VEZsCM8lwfhaWrQi8dUOAkicYHxUHaSAyM-FzL34wXI,3583
|
|
5
|
-
ducktools/classbuilder/annotations.pyi,sha256=c5vYtULdDgMYWtkzeYMsHIbmnEuT2Ru-nNZieWvYuQ4,247
|
|
6
|
-
ducktools/classbuilder/prefab.py,sha256=I1FRb1avVp6u1TPJgEs5QB1pnRH0xh8PQoI3oF8su5Q,24415
|
|
7
|
-
ducktools/classbuilder/prefab.pyi,sha256=sd800Hx2jf3CIaIfcurzanKGV-7-Km6_dY8JrM2FRHw,6363
|
|
8
|
-
ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
|
|
9
|
-
ducktools_classbuilder-0.9.0.dist-info/LICENSE,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
|
|
10
|
-
ducktools_classbuilder-0.9.0.dist-info/METADATA,sha256=l6eUeDAEMjJKTInGL2Q7oXGSfj0ur0sKy8UWdbtB15c,9636
|
|
11
|
-
ducktools_classbuilder-0.9.0.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
|
12
|
-
ducktools_classbuilder-0.9.0.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
|
|
13
|
-
ducktools_classbuilder-0.9.0.dist-info/RECORD,,
|
{ducktools_classbuilder-0.9.0.dist-info → ducktools_classbuilder-0.10.0.dist-info/licenses}/LICENSE
RENAMED
|
File without changes
|
{ducktools_classbuilder-0.9.0.dist-info → ducktools_classbuilder-0.10.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|