ducktools-classbuilder 0.8.0__tar.gz → 0.8.2__tar.gz

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.

Files changed (92) hide show
  1. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/MANIFEST.in +1 -0
  2. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/PKG-INFO +2 -24
  3. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs/extension_examples.md +19 -0
  4. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs/index.md +4 -5
  5. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/pyproject.toml +0 -1
  6. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/src/ducktools/classbuilder/__init__.py +15 -5
  7. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/src/ducktools/classbuilder/__init__.pyi +3 -0
  8. ducktools_classbuilder-0.8.2/src/ducktools/classbuilder/_version.py +2 -0
  9. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/src/ducktools/classbuilder/annotations.py +47 -10
  10. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/src/ducktools/classbuilder/prefab.py +6 -1
  11. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/src/ducktools/classbuilder/prefab.pyi +5 -2
  12. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/src/ducktools_classbuilder.egg-info/PKG-INFO +2 -24
  13. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/src/ducktools_classbuilder.egg-info/SOURCES.txt +1 -5
  14. ducktools_classbuilder-0.8.0/perf/cluegen.py +0 -127
  15. ducktools_classbuilder-0.8.0/perf/dataklasses.py +0 -102
  16. ducktools_classbuilder-0.8.0/perf/hyperfine_testmaker.py +0 -311
  17. ducktools_classbuilder-0.8.0/perf/perf_profile.py +0 -291
  18. ducktools_classbuilder-0.8.0/src/ducktools/classbuilder/_version.py +0 -2
  19. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/.github/dependabot.yml +0 -0
  20. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/.github/workflows/auto_test.yml +0 -0
  21. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/.github/workflows/publish_to_pypi.yml +0 -0
  22. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/.github/workflows/publish_to_testpypi.yml +0 -0
  23. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/.gitignore +0 -0
  24. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/.readthedocs.yaml +0 -0
  25. /ducktools_classbuilder-0.8.0/LICENSE.md → /ducktools_classbuilder-0.8.2/LICENSE +0 -0
  26. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/README.md +0 -0
  27. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs/Makefile +0 -0
  28. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs/api.md +0 -0
  29. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs/approach_vs_tool.md +0 -0
  30. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs/conf.py +0 -0
  31. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs/generated_code.md +0 -0
  32. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs/make.bat +0 -0
  33. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs/perf/performance_tests.md +0 -0
  34. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs/prefab/index.md +0 -0
  35. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs/tutorial.md +0 -0
  36. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs_code/docs_ex10_frozen_attributes.py +0 -0
  37. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs_code/docs_ex1_basic.py +0 -0
  38. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs_code/docs_ex2_register.py +0 -0
  39. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs_code/docs_ex3_iterable.py +0 -0
  40. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs_code/docs_ex5_frozen.py +0 -0
  41. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs_code/docs_ex7_posonly.py +0 -0
  42. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs_code/docs_ex8_converters.py +0 -0
  43. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs_code/docs_ex9_annotated.py +0 -0
  44. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs_code/index_example.py +0 -0
  45. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/docs_code/tutorial_code.py +0 -0
  46. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/setup.cfg +0 -0
  47. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/src/ducktools/classbuilder/annotations.pyi +0 -0
  48. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/src/ducktools/classbuilder/py.typed +0 -0
  49. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
  50. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/src/ducktools_classbuilder.egg-info/requires.txt +0 -0
  51. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
  52. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/annotations/test_annotated.py +0 -0
  53. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/annotations/test_annotations_module.py +0 -0
  54. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/annotations/test_future_annotations.py +0 -0
  55. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/conftest.py +0 -0
  56. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/dynamic/test_compare_attrib.py +0 -0
  57. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/dynamic/test_construction.py +0 -0
  58. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/dynamic/test_frozen.py +0 -0
  59. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/dynamic/test_internals.py +0 -0
  60. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/dynamic/test_pre_post_init.py +0 -0
  61. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/dynamic/test_private.py +0 -0
  62. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/dynamic/test_slots_novalues.py +0 -0
  63. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/dynamic/test_slotted_class.py +0 -0
  64. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/dynamic/test_subclass_implementation.py +0 -0
  65. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/conftest.py +0 -0
  66. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/examples/creation.py +0 -0
  67. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/examples/creation_empty.py +0 -0
  68. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/examples/dunders.py +0 -0
  69. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/examples/fails/creation_2.py +0 -0
  70. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/examples/fails/creation_3.py +0 -0
  71. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/examples/fails/creation_5.py +0 -0
  72. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/examples/fails/inheritance_1.py +0 -0
  73. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/examples/fails/inheritance_2.py +0 -0
  74. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/examples/funcs_prefabs.py +0 -0
  75. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/examples/hint_syntax.py +0 -0
  76. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/examples/inheritance.py +0 -0
  77. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/examples/init_ex.py +0 -0
  78. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/examples/kw_only.py +0 -0
  79. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/examples/repr_func.py +0 -0
  80. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/test_creation.py +0 -0
  81. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/test_dunders.py +0 -0
  82. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/test_funcs.py +0 -0
  83. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/test_hint_syntax.py +0 -0
  84. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/test_inheritance.py +0 -0
  85. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/test_init.py +0 -0
  86. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/test_kw_only.py +0 -0
  87. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/prefab/shared/test_repr.py +0 -0
  88. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/py312_tests/test_generic_annotations.py +0 -0
  89. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/py314_tests/test_forwardref_annotations.py +0 -0
  90. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/test_core.py +0 -0
  91. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/test_field_flags.py +0 -0
  92. {ducktools_classbuilder-0.8.0 → ducktools_classbuilder-0.8.2}/tests/test_slotmakermeta.py +0 -0
@@ -5,5 +5,6 @@ graft docs
5
5
  prune */build
6
6
  prune */dist
7
7
  prune */.pytest_cache
8
+ prune perf
8
9
 
9
10
  global-exclude *~ *.py[cod] *.so
@@ -1,30 +1,8 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.8.0
3
+ Version: 0.8.2
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
- License: MIT License
7
-
8
- Copyright (c) 2024 David C Ellis
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
28
6
  Project-URL: Homepage, https://github.com/davidcellis/ducktools-classbuilder
29
7
  Classifier: Development Status :: 4 - Beta
30
8
  Classifier: Programming Language :: Python :: 3.8
@@ -37,7 +15,7 @@ Classifier: Operating System :: OS Independent
37
15
  Classifier: License :: OSI Approved :: MIT License
38
16
  Requires-Python: >=3.8
39
17
  Description-Content-Type: text/markdown
40
- License-File: LICENSE.md
18
+ License-File: LICENSE
41
19
  Provides-Extra: testing
42
20
  Requires-Dist: pytest>=8.2; extra == "testing"
43
21
  Requires-Dist: pytest-cov; extra == "testing"
@@ -116,6 +116,25 @@ You could also choose to yield tuples of `name, value` pairs in your implementat
116
116
 
117
117
  ### Extending Field ###
118
118
 
119
+ The `Field` class can also be extended as if it is a slotclass, with annotations or
120
+ with `Field` declarations.
121
+
122
+ One notable caveat - if you want to use a `default_factory` in extending `Field` you
123
+ need to declare `default=FIELD_NOTHING` also in order for default to be ignored. This
124
+ is a special case for `Field` and is not needed in general.
125
+
126
+ ```python
127
+ from ducktools.classbuilder import Field, FIELD_NOTHING
128
+
129
+ class MetadataField(Field):
130
+ metadata: dict = Field(default=FIELD_NOTHING, default_factory=dict)
131
+ ```
132
+
133
+ In regular classes the `__init__` function generator considers `NOTHING` to be an
134
+ ignored value, but for `Field` subclasses it is a valid value so `FIELD_NOTHING` is
135
+ the ignored term. This is all because `None` *is* a valid value and can't be used
136
+ as a sentinel for Fields (otherwise `Field(default=None)` couldn't work).
137
+
119
138
  #### Positional Only Arguments? ####
120
139
 
121
140
  This is possible, but a little longer as we also need to modify multiple methods
@@ -77,11 +77,11 @@ is created.
77
77
 
78
78
  ```python
79
79
  from ducktools.classbuilder import (
80
- SlotMakerMeta,
81
- annotation_gatherer,
80
+ SlotMakerMeta,
82
81
  builder,
83
82
  check_argument_order,
84
83
  default_methods,
84
+ unified_gatherer,
85
85
  )
86
86
 
87
87
 
@@ -91,7 +91,7 @@ class AnnotationClass(metaclass=SlotMakerMeta):
91
91
  def __init_subclass__(
92
92
  cls,
93
93
  methods=default_methods,
94
- gatherer=annotation_gatherer,
94
+ gatherer=unified_gatherer,
95
95
  **kwargs
96
96
  ):
97
97
  # Check class dict otherwise this will always be True as this base
@@ -103,12 +103,11 @@ class AnnotationClass(metaclass=SlotMakerMeta):
103
103
  super().__init_subclass__(**kwargs)
104
104
 
105
105
 
106
-
107
106
  class AnnotatedDC(AnnotationClass):
108
107
  the_answer: int = 42
109
108
  the_question: str = "What do you get if you multiply six by nine?"
110
109
 
111
-
110
+
112
111
  ex = AnnotatedDC()
113
112
  print(ex)
114
113
  ```
@@ -25,7 +25,6 @@ classifiers = [
25
25
  "License :: OSI Approved :: MIT License",
26
26
  ]
27
27
  dynamic = ['version']
28
- license = {file = "LICENSE.md"}
29
28
 
30
29
  [project.optional-dependencies]
31
30
  testing = ["pytest>=8.2", "pytest-cov", "typing_extensions"]
@@ -99,11 +99,16 @@ def _get_inst_fields(inst):
99
99
  # As 'None' can be a meaningful value we need a sentinel value
100
100
  # to use to show no value has been provided.
101
101
  class _NothingType:
102
+ def __init__(self, custom=None):
103
+ self.custom = custom
102
104
  def __repr__(self):
105
+ if self.custom:
106
+ return f"<{self.custom} NOTHING OBJECT>"
103
107
  return "<NOTHING OBJECT>"
104
108
 
105
109
 
106
110
  NOTHING = _NothingType()
111
+ FIELD_NOTHING = _NothingType("FIELD")
107
112
 
108
113
 
109
114
  # KW_ONLY sentinel 'type' to use to indicate all subsequent attributes are
@@ -432,11 +437,11 @@ frozen_setattr_maker = MethodMaker("__setattr__", frozen_setattr_generator)
432
437
  frozen_delattr_maker = MethodMaker("__delattr__", frozen_delattr_generator)
433
438
  default_methods = frozenset({init_maker, repr_maker, eq_maker})
434
439
 
435
- # Special `__init__` maker for 'Field' subclasses
440
+ # Special `__init__` maker for 'Field' subclasses - needs its own NOTHING option
436
441
  _field_init_maker = MethodMaker(
437
442
  funcname="__init__",
438
443
  code_generator=get_init_generator(
439
- null=_NothingType(),
444
+ null=FIELD_NOTHING,
440
445
  extra_code=["self.validate_field()"],
441
446
  )
442
447
  )
@@ -649,7 +654,7 @@ class Field(metaclass=SlotMakerMeta):
649
654
 
650
655
  def validate_field(self):
651
656
  cls_name = self.__class__.__name__
652
- if self.default is not NOTHING and self.default_factory is not NOTHING:
657
+ if type(self.default) is not _NothingType and type(self.default_factory) is not _NothingType:
653
658
  raise AttributeError(
654
659
  f"{cls_name} cannot define both a default value and a default factory."
655
660
  )
@@ -807,6 +812,7 @@ def make_annotation_gatherer(
807
812
  def make_field_gatherer(
808
813
  field_type=Field,
809
814
  leave_default_values=False,
815
+ assign_types=True,
810
816
  ):
811
817
  def field_attribute_gatherer(cls_or_ns):
812
818
  if isinstance(cls_or_ns, (_MappingProxyType, dict)):
@@ -819,7 +825,11 @@ def make_field_gatherer(
819
825
  for k, v in cls_dict.items()
820
826
  if isinstance(v, field_type)
821
827
  }
822
- cls_annotations = get_ns_annotations(cls_dict)
828
+
829
+ if assign_types:
830
+ cls_annotations = get_ns_annotations(cls_dict)
831
+ else:
832
+ cls_annotations = {}
823
833
 
824
834
  cls_modifications = {}
825
835
 
@@ -830,7 +840,7 @@ def make_field_gatherer(
830
840
  else:
831
841
  cls_modifications[name] = NOTHING
832
842
 
833
- if (anno := cls_annotations.get(name, NOTHING)) is not NOTHING:
843
+ if assign_types and (anno := cls_annotations.get(name, NOTHING)) is not NOTHING:
834
844
  cls_attributes[name] = field_type.from_field(attrib, type=anno)
835
845
 
836
846
  return cls_attributes, cls_modifications
@@ -24,8 +24,10 @@ def get_methods(cls: type) -> types.MappingProxyType[str, MethodMaker]: ...
24
24
  def _get_inst_fields(inst: typing.Any) -> dict[str, typing.Any]: ...
25
25
 
26
26
  class _NothingType:
27
+ def __init__(self, custom: str | None = ...) -> None: ...
27
28
  def __repr__(self) -> str: ...
28
29
  NOTHING: _NothingType
30
+ FIELD_NOTHING: _NothingType
29
31
 
30
32
  # noinspection PyPep8Naming
31
33
  class _KW_ONLY_TYPE:
@@ -191,6 +193,7 @@ def make_annotation_gatherer(
191
193
  def make_field_gatherer(
192
194
  field_type: type[_FieldType],
193
195
  leave_default_values: bool = False,
196
+ assign_types: bool = True,
194
197
  ) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
195
198
 
196
199
  @typing.overload
@@ -0,0 +1,2 @@
1
+ __version__ = "0.8.2"
2
+ __version_tuple__ = (0, 8, 2)
@@ -22,6 +22,44 @@
22
22
  import sys
23
23
 
24
24
 
25
+ class _LazyAnnotationLib:
26
+ def __init__(self):
27
+ if sys.version_info < (3, 14):
28
+ self.annotationlib_unavailable = True
29
+ else:
30
+ self.annotationlib_unavailable = None
31
+
32
+ def __getattr__(self, item):
33
+ if self.annotationlib_unavailable:
34
+ raise ImportError("'annotationlib' is not available")
35
+
36
+ try:
37
+ import annotationlib
38
+ except ImportError:
39
+ self.annotationlib_unavailable = True
40
+ raise ImportError("'annotationlib' is not available")
41
+ else:
42
+ self.Format = annotationlib.Format
43
+ self.call_annotate_function = annotationlib.call_annotate_function
44
+
45
+ # This function keeps getting changed and renamed
46
+ get_ns_annotate = getattr(annotationlib, "get_annotate_from_class_namespace", None)
47
+ if get_ns_annotate is None:
48
+ get_ns_annotate = getattr(annotationlib, "get_annotate_function")
49
+ self.get_ns_annotate = get_ns_annotate
50
+
51
+ if item == "Format":
52
+ return self.Format
53
+ elif item == "call_annotate_function":
54
+ return self.call_annotate_function
55
+ elif item == "get_ns_annotate":
56
+ return get_ns_annotate
57
+
58
+ raise AttributeError(f"{item!r} is not available from this lazy importer")
59
+
60
+ _lazy_annotationlib = _LazyAnnotationLib()
61
+
62
+
25
63
  def get_ns_annotations(ns):
26
64
  """
27
65
  Given a class namespace, attempt to retrieve the
@@ -35,19 +73,18 @@ def get_ns_annotations(ns):
35
73
  if annotations is not None:
36
74
  annotations = annotations.copy()
37
75
  else:
38
- # See if we're using PEP-649 annotations
39
- # Guarding this with a try/except instead of a version check
40
- # In case there's a change and PEP-649 somehow doesn't make 3.14
41
76
  try:
42
- from annotationlib import Format, call_annotate_function, get_annotate_function
43
- except ImportError:
44
- pass
45
- else:
46
- annotate = ns.get("__annotate__") # Works in the alphas, but may break
77
+ # See if we're using PEP-649 annotations
78
+ annotate = ns.get("__annotate__") # Works in the early alphas
47
79
  if not annotate:
48
- annotate = get_annotate_function(ns)
80
+ annotate = _lazy_annotationlib.get_ns_annotate(ns)
49
81
  if annotate:
50
- annotations = call_annotate_function(annotate, format=Format.FORWARDREF)
82
+ annotations = _lazy_annotationlib.call_annotate_function(
83
+ annotate,
84
+ format=_lazy_annotationlib.Format.FORWARDREF
85
+ )
86
+ except ImportError:
87
+ pass
51
88
 
52
89
  if annotations is None:
53
90
  annotations = {}
@@ -26,7 +26,7 @@ A 'prebuilt' implementation of class generation.
26
26
  Includes pre and post init functions along with other methods.
27
27
  """
28
28
  from . import (
29
- INTERNALS_DICT, NOTHING,
29
+ INTERNALS_DICT, NOTHING, FIELD_NOTHING,
30
30
  Field, MethodMaker, GatheredFields, GeneratedCode, SlotMakerMeta,
31
31
  builder, get_flags, get_fields,
32
32
  make_unified_gatherer,
@@ -289,10 +289,12 @@ class Attribute(Field):
289
289
  :param kw_only: Make this argument keyword only in init
290
290
  :param serialize: Include this attribute in methods that serialize to dict
291
291
  :param doc: Parameter documentation for slotted classes
292
+ :param metadata: Additional non-construction related metadata
292
293
  :param type: Type of this attribute (for slotted classes)
293
294
  """
294
295
  iter: bool = True
295
296
  serialize: bool = True
297
+ metadata: dict = Field(default=FIELD_NOTHING, default_factory=dict)
296
298
 
297
299
 
298
300
  # noinspection PyShadowingBuiltins
@@ -309,6 +311,7 @@ def attribute(
309
311
  exclude_field=False,
310
312
  private=False,
311
313
  doc=None,
314
+ metadata=None,
312
315
  type=NOTHING,
313
316
  ):
314
317
  """
@@ -326,6 +329,7 @@ def attribute(
326
329
  :param exclude_field: Shorthand for setting repr, compare, iter and serialize to False
327
330
  :param private: Short for init, repr, compare, iter, serialize = False, must have default or factory
328
331
  :param doc: Parameter documentation for slotted classes
332
+ :param metadata: Dictionary for additional non-construction metadata
329
333
  :param type: Type of this attribute (for slotted classes)
330
334
 
331
335
  :return: Attribute generated with these parameters.
@@ -356,6 +360,7 @@ def attribute(
356
360
  serialize=serialize,
357
361
  doc=doc,
358
362
  type=type,
363
+ metadata=metadata,
359
364
  )
360
365
 
361
366
 
@@ -50,6 +50,7 @@ class Attribute(Field):
50
50
 
51
51
  iter: bool
52
52
  serialize: bool
53
+ metadata: dict
53
54
 
54
55
  def __init__(
55
56
  self,
@@ -64,6 +65,7 @@ class Attribute(Field):
64
65
  iter: bool = True,
65
66
  kw_only: bool = False,
66
67
  serialize: bool = True,
68
+ metadata: dict | None = None,
67
69
  ) -> None: ...
68
70
 
69
71
  def __repr__(self) -> str: ...
@@ -74,8 +76,6 @@ def attribute(
74
76
  *,
75
77
  default: typing.Any | _NothingType = NOTHING,
76
78
  default_factory: typing.Any | _NothingType = NOTHING,
77
- type: type | _NothingType = NOTHING,
78
- doc: str | None = None,
79
79
  init: bool = True,
80
80
  repr: bool = True,
81
81
  compare: bool = True,
@@ -84,6 +84,9 @@ def attribute(
84
84
  serialize: bool = True,
85
85
  exclude_field: bool = False,
86
86
  private: bool = False,
87
+ doc: str | None = None,
88
+ metadata: dict | None = None,
89
+ type: type | _NothingType = NOTHING,
87
90
  ) -> Attribute: ...
88
91
 
89
92
  def prefab_gatherer(cls_or_ns: type | MappingProxyType) -> tuple[dict[str, Attribute], dict[str, typing.Any]]: ...
@@ -1,30 +1,8 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.8.0
3
+ Version: 0.8.2
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
- License: MIT License
7
-
8
- Copyright (c) 2024 David C Ellis
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
28
6
  Project-URL: Homepage, https://github.com/davidcellis/ducktools-classbuilder
29
7
  Classifier: Development Status :: 4 - Beta
30
8
  Classifier: Programming Language :: Python :: 3.8
@@ -37,7 +15,7 @@ Classifier: Operating System :: OS Independent
37
15
  Classifier: License :: OSI Approved :: MIT License
38
16
  Requires-Python: >=3.8
39
17
  Description-Content-Type: text/markdown
40
- License-File: LICENSE.md
18
+ License-File: LICENSE
41
19
  Provides-Extra: testing
42
20
  Requires-Dist: pytest>=8.2; extra == "testing"
43
21
  Requires-Dist: pytest-cov; extra == "testing"
@@ -1,6 +1,6 @@
1
1
  .gitignore
2
2
  .readthedocs.yaml
3
- LICENSE.md
3
+ LICENSE
4
4
  MANIFEST.in
5
5
  README.md
6
6
  pyproject.toml
@@ -29,10 +29,6 @@ docs_code/docs_ex8_converters.py
29
29
  docs_code/docs_ex9_annotated.py
30
30
  docs_code/index_example.py
31
31
  docs_code/tutorial_code.py
32
- perf/cluegen.py
33
- perf/dataklasses.py
34
- perf/hyperfine_testmaker.py
35
- perf/perf_profile.py
36
32
  src/ducktools/classbuilder/__init__.py
37
33
  src/ducktools/classbuilder/__init__.pyi
38
34
  src/ducktools/classbuilder/_version.py
@@ -1,127 +0,0 @@
1
- # cluegen.py
2
- #
3
- # Classes generated from type clues.
4
- #
5
- # https://github.com/dabeaz/cluegen
6
- #
7
- # Author: David Beazley (@dabeaz).
8
- # http://www.dabeaz.com
9
- #
10
- # Copyright (C) 2018-2021.
11
- #
12
- # Permission is granted to use, copy, and modify this code in any
13
- # manner as long as this copyright message and disclaimer remain in
14
- # the source code. There is no warranty. Try to use the code for the
15
- # greater good.
16
-
17
- import types
18
-
19
-
20
- # Collect all type clues from a class and base classes.
21
- def all_clues(cls):
22
- clues = {}
23
- for c in reversed(cls.__mro__):
24
- clues.update(getattr(c, '__annotations__', {}))
25
- return clues
26
-
27
-
28
- # Decorator to define methods of a class as a code generator.
29
- def cluegen(func):
30
- def __get__(self, instance, cls):
31
- locs = {}
32
- code = func(cls)
33
- exec(code, locs)
34
- meth = locs[func.__name__]
35
- setattr(cls, func.__name__, meth)
36
- return meth.__get__(instance, cls)
37
-
38
- def __set_name__(self, cls, name):
39
- methods = cls.__dict__.get('_methods', list(cls._methods))
40
- if '_methods' not in cls.__dict__:
41
- cls._methods = methods
42
- cls._methods.append((name, self))
43
-
44
- return type(f'ClueGen_{func.__name__}', (), dict(__get__=__get__,
45
- __set_name__=__set_name__))()
46
-
47
-
48
- # Base class for defining data structures
49
- class DatumBase:
50
- __slots__ = ()
51
- _methods = []
52
-
53
- @classmethod
54
- def __init_subclass__(cls):
55
- submethods = []
56
- for name, val in cls._methods:
57
- if name not in cls.__dict__:
58
- setattr(cls, name, val)
59
- submethods.append((name, val))
60
- elif val is cls.__dict__[name]:
61
- submethods.append((name, val))
62
-
63
- if submethods != cls._methods:
64
- cls._methods = submethods
65
-
66
-
67
- class Datum(DatumBase):
68
- __slots__ = ()
69
-
70
- @classmethod
71
- def __init_subclass__(cls):
72
- super().__init_subclass__()
73
- cls.__match_args__ = tuple(all_clues(cls))
74
-
75
- @cluegen
76
- def __init__(cls):
77
- clues = all_clues(cls)
78
- args = ', '.join(f'{name}={getattr(cls, name)!r}'
79
- if hasattr(cls, name) and not isinstance(getattr(cls, name),
80
- types.MemberDescriptorType) else name
81
- for name in clues)
82
- body = '\n'.join(f' self.{name} = {name}'
83
- for name in clues)
84
- return f'def __init__(self, {args}):\n{body}\n'
85
-
86
- @cluegen
87
- def __repr__(cls):
88
- clues = all_clues(cls)
89
- fmt = ', '.join('%s={self.%s!r}' % (name, name) for name in clues)
90
- return 'def __repr__(self):\n' \
91
- ' return f"{type(self).__name__}(%s)"' % fmt
92
-
93
- @cluegen
94
- def __iter__(cls):
95
- clues = all_clues(cls)
96
- values = '\n'.join(f' yield self.{name}' for name in clues)
97
- return 'def __iter__(self):\n' + values
98
-
99
- @cluegen
100
- def __eq__(cls):
101
- clues = all_clues(cls)
102
- selfvals = ','.join(f'self.{name}' for name in clues)
103
- othervals = ','.join(f'other.{name}' for name in clues)
104
- return 'def __eq__(self, other):\n' \
105
- ' if self.__class__ is other.__class__:\n' \
106
- f' return ({selfvals},) == ({othervals},)\n' \
107
- ' else:\n' \
108
- ' return NotImplemented\n'
109
-
110
- @cluegen
111
- def __hash__(cls):
112
- clues = all_clues(cls)
113
- if clues:
114
- self_tuple = '(' + ','.join(f'self.{name}' for name in clues) + ',)'
115
- else:
116
- self_tuple = '()'
117
- return 'def __hash__(self):\n' \
118
- f' return hash({self_tuple})\n'
119
-
120
-
121
- # Example use
122
- if __name__ == '__main__':
123
- # Start defining classes
124
- class Coordinates(Datum):
125
- x: int
126
- y: int
127
-
@@ -1,102 +0,0 @@
1
- # dataklasses.py
2
- #
3
- # https://github.com/dabeaz/dataklasses
4
- #
5
- # Author: David Beazley (@dabeaz).
6
- # http://www.dabeaz.com
7
- #
8
- # Copyright (C) 2021-2022.
9
- #
10
- # Permission is granted to use, copy, and modify this code in any
11
- # manner as long as this copyright message and disclaimer remain in
12
- # the source code. There is no warranty. Try to use the code for the
13
- # greater good.
14
-
15
- __all__ = ['dataklass']
16
-
17
- from functools import lru_cache, reduce
18
-
19
-
20
- def codegen(func):
21
- @lru_cache
22
- def make_func_code(numfields):
23
- names = [f'_{n}' for n in range(numfields)]
24
- exec(func(names), globals(), d := {})
25
- return d.popitem()[1]
26
-
27
- return make_func_code
28
-
29
-
30
- def patch_args_and_attributes(func, fields, start=0):
31
- return type(func)(func.__code__.replace(
32
- co_names=(*func.__code__.co_names[:start], *fields),
33
- co_varnames=('self', *fields),
34
- ), func.__globals__)
35
-
36
-
37
- def patch_attributes(func, fields, start=0):
38
- return type(func)(func.__code__.replace(
39
- co_names=(*func.__code__.co_names[:start], *fields)
40
- ), func.__globals__)
41
-
42
-
43
- def all_hints(cls):
44
- return reduce(lambda x, y: getattr(y, '__annotations__', {}) | x, cls.__mro__, {})
45
-
46
-
47
- @codegen
48
- def make__init__(fields):
49
- code = 'def __init__(self, ' + ','.join(fields) + '):\n'
50
- return code + '\n'.join(f' self.{name} = {name}\n' for name in fields)
51
-
52
-
53
- @codegen
54
- def make__repr__(fields):
55
- return 'def __repr__(self):\n' \
56
- ' return f"{type(self).__name__}(' + \
57
- ', '.join('{self.' + name + '!r}' for name in fields) + ')"\n'
58
-
59
-
60
- @codegen
61
- def make__eq__(fields):
62
- selfvals = ','.join(f'self.{name}' for name in fields)
63
- othervals = ','.join(f'other.{name}' for name in fields)
64
- return 'def __eq__(self, other):\n' \
65
- ' if self.__class__ is other.__class__:\n' \
66
- f' return ({selfvals},) == ({othervals},)\n' \
67
- ' else:\n' \
68
- ' return NotImplemented\n'
69
-
70
-
71
- @codegen
72
- def make__iter__(fields):
73
- return 'def __iter__(self):\n' + '\n'.join(f' yield self.{name}' for name in fields)
74
-
75
-
76
- @codegen
77
- def make__hash__(fields):
78
- self_tuple = '(' + ','.join(f'self.{name}' for name in fields) + ',)'
79
- return 'def __hash__(self):\n' \
80
- f' return hash({self_tuple})\n'
81
-
82
-
83
- def dataklass(cls):
84
- fields = all_hints(cls)
85
- nfields = len(fields)
86
- clsdict = vars(cls)
87
- if not '__init__' in clsdict: cls.__init__ = patch_args_and_attributes(make__init__(nfields), fields)
88
- if not '__repr__' in clsdict: cls.__repr__ = patch_attributes(make__repr__(nfields), fields, 2)
89
- if not '__eq__' in clsdict: cls.__eq__ = patch_attributes(make__eq__(nfields), fields, 1)
90
- # if not '__iter__' in clsdict: cls.__iter__ = patch_attributes(make__iter__(nfields), fields)
91
- # if not '__hash__' in clsdict: cls.__hash__ = patch_attributes(make__hash__(nfields), fields, 1)
92
- cls.__match_args__ = tuple(fields)
93
- return cls
94
-
95
-
96
- # Example use
97
- if __name__ == '__main__':
98
- @dataklass
99
- class Coordinates:
100
- x: int
101
- y: int
102
-