ducktools-classbuilder 0.8.4__tar.gz → 0.9.1__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 (90) hide show
  1. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/.github/workflows/auto_test.yml +1 -1
  2. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/PKG-INFO +2 -1
  3. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/pyproject.toml +5 -1
  4. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/src/ducktools/classbuilder/__init__.py +32 -19
  5. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/src/ducktools/classbuilder/__init__.pyi +2 -13
  6. ducktools_classbuilder-0.9.1/src/ducktools/classbuilder/_version.py +2 -0
  7. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/src/ducktools/classbuilder/annotations.py +1 -1
  8. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/src/ducktools/classbuilder/prefab.py +10 -3
  9. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/src/ducktools/classbuilder/prefab.pyi +65 -24
  10. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/src/ducktools_classbuilder.egg-info/PKG-INFO +2 -1
  11. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/src/ducktools_classbuilder.egg-info/SOURCES.txt +1 -0
  12. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/annotations/test_annotated.py +1 -42
  13. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/dynamic/test_subclass_implementation.py +47 -1
  14. ducktools_classbuilder-0.9.1/tests/prefab/test_replace.py +65 -0
  15. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/test_core.py +0 -47
  16. ducktools_classbuilder-0.8.4/src/ducktools/classbuilder/_version.py +0 -2
  17. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/.github/dependabot.yml +0 -0
  18. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/.github/workflows/publish_to_pypi.yml +0 -0
  19. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/.github/workflows/publish_to_testpypi.yml +0 -0
  20. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/.gitignore +0 -0
  21. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/.readthedocs.yaml +0 -0
  22. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/LICENSE +0 -0
  23. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/MANIFEST.in +0 -0
  24. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/README.md +0 -0
  25. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs/Makefile +0 -0
  26. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs/api.md +0 -0
  27. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs/approach_vs_tool.md +0 -0
  28. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs/conf.py +0 -0
  29. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs/extension_examples.md +0 -0
  30. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs/generated_code.md +0 -0
  31. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs/index.md +0 -0
  32. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs/make.bat +0 -0
  33. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs/perf/performance_tests.md +0 -0
  34. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs/prefab/index.md +0 -0
  35. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs/tutorial.md +0 -0
  36. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs_code/docs_ex10_frozen_attributes.py +0 -0
  37. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs_code/docs_ex1_basic.py +0 -0
  38. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs_code/docs_ex2_register.py +0 -0
  39. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs_code/docs_ex3_iterable.py +0 -0
  40. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs_code/docs_ex5_frozen.py +0 -0
  41. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs_code/docs_ex7_posonly.py +0 -0
  42. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs_code/docs_ex8_converters.py +0 -0
  43. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs_code/docs_ex9_annotated.py +0 -0
  44. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs_code/index_example.py +0 -0
  45. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/docs_code/tutorial_code.py +0 -0
  46. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/setup.cfg +0 -0
  47. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/src/ducktools/classbuilder/annotations.pyi +0 -0
  48. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/src/ducktools/classbuilder/py.typed +0 -0
  49. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
  50. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/src/ducktools_classbuilder.egg-info/requires.txt +0 -0
  51. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
  52. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/annotations/test_annotations_module.py +0 -0
  53. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/annotations/test_future_annotations.py +0 -0
  54. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/conftest.py +0 -0
  55. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/dynamic/test_compare_attrib.py +0 -0
  56. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/dynamic/test_construction.py +0 -0
  57. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/dynamic/test_frozen.py +0 -0
  58. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/dynamic/test_internals.py +0 -0
  59. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/dynamic/test_pre_post_init.py +0 -0
  60. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/dynamic/test_private.py +0 -0
  61. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/dynamic/test_slots_novalues.py +0 -0
  62. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/dynamic/test_slotted_class.py +0 -0
  63. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/conftest.py +0 -0
  64. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/examples/creation.py +0 -0
  65. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/examples/creation_empty.py +0 -0
  66. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/examples/dunders.py +0 -0
  67. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/examples/fails/creation_2.py +0 -0
  68. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/examples/fails/creation_3.py +0 -0
  69. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/examples/fails/creation_5.py +0 -0
  70. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/examples/fails/inheritance_1.py +0 -0
  71. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/examples/fails/inheritance_2.py +0 -0
  72. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/examples/funcs_prefabs.py +0 -0
  73. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/examples/hint_syntax.py +0 -0
  74. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/examples/inheritance.py +0 -0
  75. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/examples/init_ex.py +0 -0
  76. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/examples/kw_only.py +0 -0
  77. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/examples/repr_func.py +0 -0
  78. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/test_creation.py +0 -0
  79. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/test_dunders.py +0 -0
  80. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/test_funcs.py +0 -0
  81. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/test_hint_syntax.py +0 -0
  82. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/test_inheritance.py +0 -0
  83. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/test_init.py +0 -0
  84. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/test_kw_only.py +0 -0
  85. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/prefab/shared/test_repr.py +0 -0
  86. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/py312_tests/test_generic_annotations.py +0 -0
  87. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/py314_tests/_test_support.py +0 -0
  88. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/py314_tests/test_forwardref_annotations.py +0 -0
  89. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/test_field_flags.py +0 -0
  90. {ducktools_classbuilder-0.8.4 → ducktools_classbuilder-0.9.1}/tests/test_slotmakermeta.py +0 -0
@@ -14,7 +14,7 @@ jobs:
14
14
  fail-fast: false
15
15
  matrix:
16
16
  os: [ubuntu-latest]
17
- python-version: ["3.14-dev", "3.13", "3.12", "3.11", "3.10", "pypy-3.10", "3.9", "3.8"]
17
+ python-version: ["3.14-dev", "3.13", "3.12", "3.11", "pypy-3.11", "3.10", "3.9", "3.8"]
18
18
 
19
19
  steps:
20
20
  - uses: actions/checkout@v4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.8.4
3
+ Version: 0.9.1
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
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.10
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
14
15
  Classifier: Operating System :: OS Independent
15
16
  Classifier: License :: OSI Approved :: MIT License
16
17
  Requires-Python: >=3.8
@@ -21,6 +21,7 @@ classifiers = [
21
21
  "Programming Language :: Python :: 3.11",
22
22
  "Programming Language :: Python :: 3.12",
23
23
  "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
24
25
  "Operating System :: OS Independent",
25
26
  "License :: OSI Approved :: MIT License",
26
27
  ]
@@ -37,7 +38,10 @@ where = ["src"]
37
38
 
38
39
  [tool.setuptools_scm]
39
40
  version_file = "src/ducktools/classbuilder/_version.py"
40
- version_file_template = "__version__ = \"{version}\"\n__version_tuple__ = {version_tuple}\n"
41
+ version_file_template = """
42
+ __version__ = "{version}"
43
+ __version_tuple__ = {version_tuple}
44
+ """
41
45
 
42
46
  [project.urls]
43
47
  "Homepage" = "https://github.com/davidcellis/ducktools-classbuilder"
@@ -47,7 +47,7 @@ _UNDER_TESTING = os.environ.get("PYTEST_VERSION") is not None
47
47
  # Obtain types the same way types.py does in pypy
48
48
  # See: https://github.com/pypy/pypy/blob/19d9fa6be11165116dd0839b9144d969ab426ae7/lib-python/3/types.py#L61-L73
49
49
  class _C: __slots__ = 's' # noqa
50
- _MemberDescriptorType = type(_C.s) # noqa
50
+ _MemberDescriptorType = type(_C.s) # type: ignore
51
51
  _MappingProxyType = type(type.__dict__)
52
52
  del _C
53
53
 
@@ -385,6 +385,36 @@ def eq_generator(cls, funcname="__eq__"):
385
385
  return GeneratedCode(code, globs)
386
386
 
387
387
 
388
+ def replace_generator(cls, funcname="__replace__"):
389
+ # Generate the replace method for built classes
390
+ # unlike the dataclasses implementation this is generated
391
+ attribs = get_fields(cls)
392
+
393
+ # This is essentially the as_dict generator for prefabs
394
+ # except based on attrib.init instead of .serialize
395
+ vals = ", ".join(
396
+ f"'{name}': self.{name}"
397
+ for name, attrib in attribs.items()
398
+ if attrib.init
399
+ )
400
+ init_dict = f"{{{vals}}}"
401
+
402
+ code = (
403
+ f"def {funcname}(self, /, **changes):\n"
404
+ f" new_kwargs = {init_dict}\n"
405
+ f" for name, value in changes.items():\n"
406
+ f" if name not in new_kwargs:\n"
407
+ f" raise TypeError(\n"
408
+ f" f\"{{name!r}} is not a valid replacable \"\n"
409
+ f" f\"field on {{self.__class__.__name__!r}}\"\n"
410
+ f" )\n"
411
+ f" new_kwargs[name] = value\n"
412
+ f" return self.__class__(**new_kwargs)\n"
413
+ )
414
+ globs = {}
415
+ return GeneratedCode(code, globs)
416
+
417
+
388
418
  def frozen_setattr_generator(cls, funcname="__setattr__"):
389
419
  globs = {}
390
420
  field_names = set(get_fields(cls))
@@ -433,6 +463,7 @@ def frozen_delattr_generator(cls, funcname="__delattr__"):
433
463
  init_maker = MethodMaker("__init__", init_generator)
434
464
  repr_maker = MethodMaker("__repr__", repr_generator)
435
465
  eq_maker = MethodMaker("__eq__", eq_generator)
466
+ replace_maker = MethodMaker("__replace__", replace_generator)
436
467
  frozen_setattr_maker = MethodMaker("__setattr__", frozen_setattr_generator)
437
468
  frozen_delattr_maker = MethodMaker("__delattr__", frozen_delattr_generator)
438
469
  default_methods = frozenset({init_maker, repr_maker, eq_maker})
@@ -961,24 +992,6 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
961
992
  return cls
962
993
 
963
994
 
964
- class AnnotationClass(metaclass=SlotMakerMeta):
965
- __slots__ = {}
966
-
967
- def __init_subclass__(
968
- cls,
969
- methods=default_methods,
970
- gatherer=unified_gatherer,
971
- **kwargs
972
- ):
973
- # Check class dict otherwise this will always be True as this base
974
- # class uses slots.
975
- slots = "__slots__" in cls.__dict__
976
-
977
- builder(cls, gatherer=gatherer, methods=methods, flags={"slotted": slots})
978
- check_argument_order(cls)
979
- super().__init_subclass__(**kwargs)
980
-
981
-
982
995
  @slotclass
983
996
  class GatheredFields:
984
997
  """
@@ -5,7 +5,6 @@ import inspect
5
5
 
6
6
  from collections.abc import Callable
7
7
  from types import MappingProxyType
8
- from typing_extensions import dataclass_transform
9
8
 
10
9
  _py_type = type | str # Alias for type hint values
11
10
  _CopiableMappings = dict[str, typing.Any] | MappingProxyType[str, typing.Any]
@@ -74,6 +73,7 @@ def get_repr_generator(
74
73
  ) -> _CodegenType: ...
75
74
  def repr_generator(cls: type, funcname: str = "__repr__") -> GeneratedCode: ...
76
75
  def eq_generator(cls: type, funcname: str = "__eq__") -> GeneratedCode: ...
76
+ def replace_generator(cls: type, funcname: str = "__replace__") -> GeneratedCode: ...
77
77
 
78
78
  def frozen_setattr_generator(cls: type, funcname: str = "__setattr__") -> GeneratedCode: ...
79
79
 
@@ -82,6 +82,7 @@ def frozen_delattr_generator(cls: type, funcname: str = "__delattr__") -> Genera
82
82
  init_maker: MethodMaker
83
83
  repr_maker: MethodMaker
84
84
  eq_maker: MethodMaker
85
+ replace_maker: MethodMaker
85
86
  frozen_setattr_maker: MethodMaker
86
87
  frozen_delattr_maker: MethodMaker
87
88
  default_methods: frozenset[MethodMaker]
@@ -244,18 +245,6 @@ def slotclass(
244
245
 
245
246
  _gatherer_type = Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]
246
247
 
247
-
248
- @dataclass_transform(field_specifiers=(Field,))
249
- class AnnotationClass(metaclass=SlotMakerMeta):
250
- __slots__: dict
251
-
252
- def __init_subclass__(
253
- cls,
254
- methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
255
- gatherer: _gatherer_type = unified_gatherer,
256
- **kwargs,
257
- ) -> None: ...
258
-
259
248
  class GatheredFields:
260
249
  __slots__: dict[str, None]
261
250
 
@@ -0,0 +1,2 @@
1
+ __version__ = "0.9.1"
2
+ __version_tuple__ = (0, 9, 1)
@@ -25,7 +25,7 @@ import sys
25
25
  class _LazyAnnotationLib:
26
26
  def __getattr__(self, item):
27
27
  global _lazyannotationlib
28
- import annotationlib # type: ignore - this is a Python 3.14 library
28
+ import annotationlib # type: ignore
29
29
  _lazyannotationlib = annotationlib
30
30
  return getattr(annotationlib, item)
31
31
 
@@ -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, eq_maker,
33
+ eq_maker, frozen_setattr_maker, frozen_delattr_maker, replace_maker,
34
34
  get_repr_generator,
35
35
  )
36
36
 
@@ -294,7 +294,7 @@ class Attribute(Field):
294
294
  """
295
295
  iter: bool = True
296
296
  serialize: bool = True
297
- metadata: dict = Field(default=FIELD_NOTHING, default_factory=dict)
297
+ metadata: dict = Field(default=FIELD_NOTHING, default_factory=dict) # type: ignore
298
298
 
299
299
 
300
300
  # noinspection PyShadowingBuiltins
@@ -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,
@@ -551,7 +553,7 @@ def _make_prefab(
551
553
 
552
554
  class Prefab(metaclass=SlotMakerMeta):
553
555
  _meta_gatherer = prefab_gatherer
554
- __slots__ = {}
556
+ __slots__ = {} # type: ignore
555
557
 
556
558
  # noinspection PyShadowingBuiltins
557
559
  def __init_subclass__(
@@ -779,3 +781,8 @@ def as_dict(o):
779
781
  for name, attrib in flds.items()
780
782
  if attrib.serialize
781
783
  }
784
+
785
+ def replace(obj, /, **changes):
786
+ if not is_prefab_instance(obj):
787
+ raise TypeError("replace() should be called on prefab instances")
788
+ return obj.__replace__(**changes)
@@ -4,7 +4,8 @@ from typing_extensions import dataclass_transform
4
4
 
5
5
  import inspect
6
6
 
7
- from collections.abc import Callable
7
+ # Suppress weird pylance error
8
+ from collections.abc import Callable # type: ignore
8
9
 
9
10
  from . import (
10
11
  NOTHING,
@@ -58,36 +59,73 @@ class Attribute(Field):
58
59
  default: typing.Any | _NothingType = NOTHING,
59
60
  default_factory: typing.Any | _NothingType = NOTHING,
60
61
  type: type | _NothingType = NOTHING,
61
- doc: str | None = None,
62
- init: bool = True,
63
- repr: bool = True,
64
- compare: bool = True,
65
- iter: bool = True,
66
- kw_only: bool = False,
67
- serialize: bool = True,
68
- metadata: dict | None = None,
62
+ doc: str | None = ...,
63
+ init: bool = ...,
64
+ repr: bool = ...,
65
+ compare: bool = ...,
66
+ iter: bool = ...,
67
+ kw_only: bool = ...,
68
+ serialize: bool = ...,
69
+ metadata: dict | None = ...,
69
70
  ) -> None: ...
70
71
 
71
72
  def __repr__(self) -> str: ...
72
73
  def __eq__(self, other: Attribute | object) -> bool: ...
73
74
  def validate_field(self) -> None: ...
74
75
 
76
+ @typing.overload
75
77
  def attribute(
76
78
  *,
77
- default: typing.Any | _NothingType = NOTHING,
78
- default_factory: typing.Any | _NothingType = NOTHING,
79
- init: bool = True,
80
- repr: bool = True,
81
- compare: bool = True,
82
- iter: bool = True,
83
- kw_only: bool = False,
84
- serialize: bool = True,
85
- exclude_field: bool = False,
86
- private: bool = False,
87
- doc: str | None = None,
88
- metadata: dict | None = None,
89
- type: type | _NothingType = NOTHING,
90
- ) -> Attribute: ...
79
+ default: _T,
80
+ default_factory: _NothingType = NOTHING,
81
+ init: bool = ...,
82
+ repr: bool = ...,
83
+ compare: bool = ...,
84
+ iter: bool = ...,
85
+ kw_only: bool = ...,
86
+ serialize: bool = ...,
87
+ exclude_field: bool = ...,
88
+ private: bool = ...,
89
+ doc: str | None = ...,
90
+ metadata: dict | None = ...,
91
+ type: type | _NothingType = ...,
92
+ ) -> _T: ...
93
+
94
+ @typing.overload
95
+ def attribute(
96
+ *,
97
+ default: _NothingType = NOTHING,
98
+ default_factory: Callable[[], _T],
99
+ init: bool = ...,
100
+ repr: bool = ...,
101
+ compare: bool = ...,
102
+ iter: bool = ...,
103
+ kw_only: bool = ...,
104
+ serialize: bool = ...,
105
+ exclude_field: bool = ...,
106
+ private: bool = ...,
107
+ doc: str | None = ...,
108
+ metadata: dict | None = ...,
109
+ type: type | _NothingType = ...,
110
+ ) -> _T: ...
111
+
112
+ @typing.overload
113
+ def attribute(
114
+ *,
115
+ default: _NothingType = NOTHING,
116
+ default_factory: _NothingType = NOTHING,
117
+ init: bool = ...,
118
+ repr: bool = ...,
119
+ compare: bool = ...,
120
+ iter: bool = ...,
121
+ kw_only: bool = ...,
122
+ serialize: bool = ...,
123
+ exclude_field: bool = ...,
124
+ private: bool = ...,
125
+ doc: str | None = ...,
126
+ metadata: dict | None = ...,
127
+ type: type | _NothingType = ...,
128
+ ) -> typing.Any: ...
91
129
 
92
130
  def prefab_gatherer(cls_or_ns: type | MappingProxyType) -> tuple[dict[str, Attribute], dict[str, typing.Any]]: ...
93
131
 
@@ -111,7 +149,8 @@ _T = typing.TypeVar("_T")
111
149
  # noinspection PyUnresolvedReferences
112
150
  @dataclass_transform(field_specifiers=(Attribute, attribute))
113
151
  class Prefab(metaclass=SlotMakerMeta):
114
- _meta_gatherer: Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]
152
+ _meta_gatherer: Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]] = ...
153
+ __slots__: dict[str, typing.Any] = ...
115
154
  def __init_subclass__(
116
155
  cls,
117
156
  init: bool = True,
@@ -199,3 +238,5 @@ def is_prefab(o: typing.Any) -> bool: ...
199
238
  def is_prefab_instance(o: object) -> bool: ...
200
239
 
201
240
  def as_dict(o) -> dict[str, typing.Any]: ...
241
+
242
+ def replace(obj: _T, /, **changes: typing.Any) -> _T: ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.8.4
3
+ Version: 0.9.1
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
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.10
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
14
15
  Classifier: Operating System :: OS Independent
15
16
  Classifier: License :: OSI Approved :: MIT License
16
17
  Requires-Python: >=3.8
@@ -49,6 +49,7 @@ tests/test_slotmakermeta.py
49
49
  tests/annotations/test_annotated.py
50
50
  tests/annotations/test_annotations_module.py
51
51
  tests/annotations/test_future_annotations.py
52
+ tests/prefab/test_replace.py
52
53
  tests/prefab/dynamic/test_compare_attrib.py
53
54
  tests/prefab/dynamic/test_construction.py
54
55
  tests/prefab/dynamic/test_frozen.py
@@ -4,14 +4,13 @@ does not interfere when wrapping ClassVar
4
4
  """
5
5
 
6
6
  import sys
7
- import pytest
8
7
 
9
8
  from typing import ClassVar
10
9
  from typing_extensions import Annotated
11
10
 
12
11
  from ducktools.classbuilder import (
13
12
  Field, SlotFields, NOTHING,
14
- AnnotationClass, annotation_gatherer, make_annotation_gatherer
13
+ annotation_gatherer, make_annotation_gatherer
15
14
  )
16
15
 
17
16
  from ducktools.classbuilder.annotations import (
@@ -102,43 +101,3 @@ def test_make_annotation_gatherer():
102
101
  for key in "defgh":
103
102
  assert key not in annos
104
103
  assert key not in modifications
105
-
106
-
107
- def test_annotationclass():
108
- class ExampleAnnotated(AnnotationClass, slots=False):
109
- a: str = "a"
110
- b: "list[str]" = "b"
111
- c: Annotated[str, ""] = Field(default="c")
112
-
113
- d: ClassVar[str] = "d"
114
- e: Annotated[ClassVar[str], ""] = "e"
115
- f: "Annotated[ClassVar[str], '']" = "f"
116
- g: Annotated[Annotated[ClassVar[str], ""], ""] = "g"
117
- h: Annotated[CV[str], ''] = "h"
118
-
119
- for key in "abc":
120
- assert key not in ExampleAnnotated.__dict__
121
-
122
- for key in "defgh":
123
- assert key in ExampleAnnotated.__dict__
124
-
125
- ex = ExampleAnnotated()
126
- for char in "abcdefgh":
127
- assert getattr(ex, char) == char
128
-
129
- ex2 = ExampleAnnotated()
130
- ex3 = ExampleAnnotated("i", "j", "k")
131
-
132
- assert ex == ex2
133
- assert ex != ex3
134
-
135
- prefix = "test_annotationclass.<locals>."
136
- assert repr(ex) == f"{prefix}ExampleAnnotated(a='a', b='b', c='c')"
137
-
138
-
139
- def test_annotated_syntax_error():
140
- with pytest.raises(SyntaxError):
141
- class ExampleAnnotated(AnnotationClass):
142
- a: str = "a" # noqa: the error being highlighted is the error we are testing.
143
- b: "list[str]"
144
- c: Annotated[str, ""] = Field(default="c")
@@ -2,7 +2,7 @@ import inspect
2
2
 
3
3
  import pytest
4
4
 
5
- from ducktools.classbuilder.prefab import Prefab, Attribute, SlotFields
5
+ from ducktools.classbuilder.prefab import Prefab, Attribute, SlotFields, get_attributes
6
6
 
7
7
 
8
8
  class TestConstructionForms:
@@ -178,3 +178,49 @@ class TestClassArguments:
178
178
  assert params['a'].kind == inspect.Parameter.KEYWORD_ONLY
179
179
  assert params['b'].kind == inspect.Parameter.KEYWORD_ONLY
180
180
  assert params['b'].default == 1
181
+
182
+
183
+ def test_slots_weakref():
184
+ import weakref
185
+
186
+ class WeakrefClass(Prefab):
187
+ a: int = 1
188
+ b: int = 2
189
+ __weakref__: dict
190
+
191
+ flds = get_attributes(WeakrefClass)
192
+ assert 'a' in flds
193
+ assert 'b' in flds
194
+ assert '__weakref__' not in flds
195
+
196
+ slots = WeakrefClass.__slots__
197
+ assert 'a' in slots
198
+ assert 'b' in slots
199
+ assert '__weakref__' in slots
200
+
201
+ # Test weakrefs can be created
202
+ inst = WeakrefClass()
203
+ ref = weakref.ref(inst)
204
+ assert ref == inst.__weakref__
205
+
206
+
207
+ def test_has_dict():
208
+ class DictClass(Prefab):
209
+ a: int = 1
210
+ b: int = 2
211
+ __dict__: dict
212
+
213
+ flds = get_attributes(DictClass)
214
+ assert 'a' in flds
215
+ assert 'b' in flds
216
+ assert '__dict__' not in flds
217
+
218
+ slots = DictClass.__slots__
219
+ assert 'a' in slots
220
+ assert 'b' in slots
221
+ assert '__dict__' in slots
222
+
223
+ # Test if __dict__ is included new values can be added
224
+ inst = DictClass()
225
+ inst.c = 42
226
+ assert inst.__dict__ == {"c": 42}
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import sys
5
+
6
+ import pytest
7
+
8
+ from ducktools.classbuilder.prefab import (
9
+ Prefab,
10
+ attribute,
11
+ prefab,
12
+ replace as prefab_replace,
13
+ )
14
+
15
+
16
+ replace_funcs = [prefab_replace]
17
+ if sys.version_info >= (3, 13): # 3.13 test against the copy version too
18
+ replace_funcs.append(copy.replace) # type: ignore
19
+
20
+
21
+ def ex_classes() -> tuple[type, type]:
22
+ @prefab
23
+ class ExDecorator:
24
+ a: int = 1
25
+ b: str = "Why?"
26
+ c: str = attribute(default="Non-replacable", init=False)
27
+
28
+ class ExBaseClass(Prefab):
29
+ a: int = 1
30
+ b: str = "Why?"
31
+ c: str = attribute(default="Non-replacable", init=False)
32
+
33
+ return ExDecorator, ExBaseClass
34
+
35
+ example_classes = ex_classes()
36
+
37
+
38
+ @pytest.mark.parametrize("replace", replace_funcs)
39
+ @pytest.mark.parametrize("ex_class", example_classes)
40
+ def test_replace_decorator(ex_class, replace):
41
+ ex = ex_class()
42
+
43
+ assert ex.a == 1
44
+ assert ex.b == "Why?"
45
+ assert ex.c == "Non-replacable"
46
+
47
+ ex_r = replace(ex, a=42)
48
+
49
+ assert ex != ex_r
50
+
51
+ assert ex_r.a == 42
52
+ assert ex_r.b == "Why?"
53
+ assert ex_r.c == "Non-replacable"
54
+
55
+
56
+ @pytest.mark.parametrize("replace", replace_funcs)
57
+ @pytest.mark.parametrize("ex_class", example_classes)
58
+ def test_replace_fail(ex_class, replace):
59
+ ex = ex_class()
60
+
61
+ with pytest.raises(TypeError):
62
+ replace(ex, c="Fails")
63
+
64
+ with pytest.raises(TypeError):
65
+ replace(ex, d="Does Not Exist")
@@ -19,7 +19,6 @@ from ducktools.classbuilder import (
19
19
  slot_gatherer,
20
20
  slotclass,
21
21
 
22
- AnnotationClass,
23
22
  Field,
24
23
  GatheredFields,
25
24
  GeneratedCode,
@@ -371,30 +370,6 @@ def test_slotclass_weakref():
371
370
  assert ref == inst.__weakref__
372
371
 
373
372
 
374
- def test_annotationclass_weakref():
375
- import weakref
376
-
377
- class WeakrefClass(AnnotationClass):
378
- a: int = 1
379
- b: int = 2
380
- __weakref__: dict
381
-
382
- flds = get_fields(WeakrefClass)
383
- assert 'a' in flds
384
- assert 'b' in flds
385
- assert '__weakref__' not in flds
386
-
387
- slots = WeakrefClass.__slots__
388
- assert 'a' in slots
389
- assert 'b' in slots
390
- assert '__weakref__' in slots
391
-
392
- # Test weakrefs can be created
393
- inst = WeakrefClass()
394
- ref = weakref.ref(inst)
395
- assert ref == inst.__weakref__
396
-
397
-
398
373
  def test_slotclass_dict():
399
374
  @slotclass
400
375
  class DictClass:
@@ -420,28 +395,6 @@ def test_slotclass_dict():
420
395
  assert inst.__dict__ == {"c": 42}
421
396
 
422
397
 
423
- def test_annotationclass_dict():
424
- class DictClass(AnnotationClass):
425
- a: int = 1
426
- b: int = 2
427
- __dict__: dict
428
-
429
- flds = get_fields(DictClass)
430
- assert 'a' in flds
431
- assert 'b' in flds
432
- assert '__dict__' not in flds
433
-
434
- slots = DictClass.__slots__
435
- assert 'a' in slots
436
- assert 'b' in slots
437
- assert '__dict__' in slots
438
-
439
- # Test if __dict__ is included new values can be added
440
- inst = DictClass()
441
- inst.c = 42
442
- assert inst.__dict__ == {"c": 42}
443
-
444
-
445
398
  def test_fieldclass():
446
399
  class NewField(Field):
447
400
  serialize: bool = True
@@ -1,2 +0,0 @@
1
- __version__ = "0.8.4"
2
- __version_tuple__ = (0, 8, 4)