ducktools-classbuilder 0.1.0__py3-none-any.whl → 0.1.1__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.

@@ -19,7 +19,7 @@
19
19
  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  # SOFTWARE.
22
- __version__ = "v0.1.0"
22
+ __version__ = "v0.1.1"
23
23
 
24
24
  # Change this name if you make heavy modifications
25
25
  INTERNALS_DICT = "__classbuilder_internals__"
@@ -415,4 +415,4 @@ def fieldclass(cls):
415
415
  methods=field_methods
416
416
  )
417
417
 
418
- return cls
418
+ return cls
@@ -1,6 +1,8 @@
1
1
  import typing
2
2
  from collections.abc import Callable
3
3
 
4
+ _py_type = type # Alias for type where it is used as a name
5
+
4
6
  __version__: str
5
7
  INTERNALS_DICT: str
6
8
 
@@ -38,14 +40,16 @@ repr_desc: MethodMaker
38
40
  eq_desc: MethodMaker
39
41
  default_methods: frozenset[MethodMaker]
40
42
 
43
+ _T = typing.TypeVar("_T")
44
+
41
45
  @typing.overload
42
46
  def builder(
43
- cls: type,
47
+ cls: type[_T],
44
48
  /,
45
49
  *,
46
50
  gatherer: Callable[[type], dict[str, Field]],
47
51
  methods: frozenset[MethodMaker] | set[MethodMaker]
48
- ) -> typing.Any: ...
52
+ ) -> type[_T]: ...
49
53
 
50
54
  @typing.overload
51
55
  def builder(
@@ -54,35 +58,30 @@ def builder(
54
58
  *,
55
59
  gatherer: Callable[[type], dict[str, Field]],
56
60
  methods: frozenset[MethodMaker] | set[MethodMaker]
57
- ) -> Callable[[type], type]: ...
58
-
61
+ ) -> Callable[[type[_T]], type[_T]]: ...
59
62
 
60
- _Self = typing.TypeVar("_Self", bound="Field")
61
63
 
62
64
  class Field:
63
65
  default: _NothingType | typing.Any
64
66
  default_factory: _NothingType | typing.Any
65
- type: _NothingType | type
67
+ type: _NothingType | _py_type
66
68
  doc: None | str
67
69
 
70
+ __classbuilder_internals__: dict
71
+
68
72
  def __init__(
69
73
  self,
70
74
  *,
71
75
  default: _NothingType | typing.Any = NOTHING,
72
76
  default_factory: _NothingType | typing.Any = NOTHING,
73
- type: _NothingType | type = NOTHING,
77
+ type: _NothingType | _py_type = NOTHING,
74
78
  doc: None | str = None,
75
79
  ) -> None: ...
76
- @property
77
- def _inherited_slots(self) -> list[str]: ...
78
80
  def __repr__(self) -> str: ...
79
- @typing.overload
80
- def __eq__(self, other: _Self) -> bool: ...
81
- @typing.overload
82
- def __eq__(self, other: object) -> NotImplemented: ...
81
+ def __eq__(self, other: Field | object) -> bool: ...
83
82
  def validate_field(self) -> None: ...
84
83
  @classmethod
85
- def from_field(cls, fld: Field, **kwargs: typing.Any) -> _Self: ...
84
+ def from_field(cls, fld: Field, /, **kwargs: typing.Any) -> Field: ...
86
85
 
87
86
 
88
87
  class SlotFields(dict):
@@ -93,19 +92,20 @@ def slot_gatherer(cls: type) -> dict[str, Field]:
93
92
 
94
93
  @typing.overload
95
94
  def slotclass(
96
- cls: type,
95
+ cls: type[_T],
97
96
  /,
98
97
  *,
99
98
  methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
100
99
  syntax_check: bool = True
101
- ) -> typing.Any: ...
100
+ ) -> type[_T]: ...
102
101
 
102
+ @typing.overload
103
103
  def slotclass(
104
104
  cls: None = None,
105
105
  /,
106
106
  *,
107
107
  methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
108
108
  syntax_check: bool = True
109
- ) -> Callable[[type], type]: ...
109
+ ) -> Callable[[type[_T]], type[_T]]: ...
110
110
 
111
- def fieldclass(cls: type) -> typing.Any: ...
111
+ def fieldclass(cls: type[_T]) -> type[_T]: ...
@@ -319,12 +319,19 @@ def get_eq_maker():
319
319
 
320
320
  def get_iter_maker():
321
321
  def __iter__(cls: "type") -> "tuple[str, dict]":
322
- field_names = get_attributes(cls).keys()
322
+ fields = get_attributes(cls)
323
323
 
324
- if field_names:
325
- values = "\n".join(f" yield self.{name} " for name in field_names)
326
- else:
324
+ valid_fields = (
325
+ name for name, attrib in fields.items()
326
+ if attrib.iter and not attrib.exclude_field
327
+ )
328
+
329
+ values = "\n".join(f" yield self.{name}" for name in valid_fields)
330
+
331
+ # if values is an empty string
332
+ if not values:
327
333
  values = " yield from ()"
334
+
328
335
  code = f"def __iter__(self):\n{values}"
329
336
  globs = {}
330
337
  return code, globs
@@ -366,6 +373,7 @@ def get_frozen_setattr_maker():
366
373
 
367
374
 
368
375
  def get_frozen_delattr_maker():
376
+ # noinspection PyUnusedLocal
369
377
  def __delattr__(cls: "type") -> "tuple[str, dict]":
370
378
  body = (
371
379
  ' raise TypeError(\n'
@@ -415,6 +423,7 @@ class Attribute(Field):
415
423
  init=True,
416
424
  repr=True,
417
425
  compare=True,
426
+ iter=True,
418
427
  kw_only=False,
419
428
  in_dict=True,
420
429
  exclude_field=False,
@@ -436,6 +445,7 @@ def attribute(
436
445
  init=True,
437
446
  repr=True,
438
447
  compare=True,
448
+ iter=True,
439
449
  kw_only=False,
440
450
  in_dict=True,
441
451
  exclude_field=False,
@@ -443,8 +453,7 @@ def attribute(
443
453
  type=NOTHING,
444
454
  ):
445
455
  """
446
- Additional definition for how to generate standard methods
447
- for an instance attribute.
456
+ Get an object to define a prefab Attribute
448
457
 
449
458
  :param default: Default value for this attribute
450
459
  :param default_factory: 0 argument callable to give a default value
@@ -452,6 +461,7 @@ def attribute(
452
461
  :param init: Include this attribute in the __init__ parameters
453
462
  :param repr: Include this attribute in the class __repr__
454
463
  :param compare: Include this attribute in the class __eq__
464
+ :param iter: Include this attribute in the class __iter__ if generated
455
465
  :param kw_only: Make this argument keyword only in init
456
466
  :param in_dict: Include this attribute in methods that serialise to dict
457
467
  :param exclude_field: Exclude this field from all magic method generation
@@ -469,6 +479,7 @@ def attribute(
469
479
  init=init,
470
480
  repr=repr,
471
481
  compare=compare,
482
+ iter=iter,
472
483
  kw_only=kw_only,
473
484
  in_dict=in_dict,
474
485
  exclude_field=exclude_field,
@@ -706,7 +717,7 @@ def _make_prefab(
706
717
  if attrib.exclude_field:
707
718
  if name not in post_init_args:
708
719
  raise PrefabError(
709
- f"{name} is an excluded attribute but is not passed to post_init"
720
+ f"{name!r} is an excluded attribute but is not passed to post_init"
710
721
  )
711
722
  else:
712
723
  valid_args.append(name)
@@ -1,4 +1,6 @@
1
1
  import typing
2
+ from typing_extensions import dataclass_transform
3
+
2
4
  from collections.abc import Callable
3
5
 
4
6
  from . import (
@@ -7,6 +9,9 @@ from . import (
7
9
  builder, fieldclass, get_internals, slot_gatherer
8
10
  )
9
11
 
12
+ # noinspection PyUnresolvedReferences
13
+ from . import _NothingType
14
+
10
15
  PREFAB_FIELDS: str
11
16
  PREFAB_INIT_FUNC: str
12
17
  PRE_INIT_FUNC: str
@@ -56,6 +61,7 @@ class Attribute(Field):
56
61
  init: bool
57
62
  repr: bool
58
63
  compare: bool
64
+ iter: bool
59
65
  kw_only: bool
60
66
  in_dict: bool
61
67
  exclude_field: bool
@@ -63,34 +69,33 @@ class Attribute(Field):
63
69
  def __init__(
64
70
  self,
65
71
  *,
66
- default: typing.Any | NOTHING =NOTHING,
67
- default_factory: typing.Any | NOTHING = NOTHING,
68
- type: type | NOTHING = NOTHING,
72
+ default: typing.Any | _NothingType = NOTHING,
73
+ default_factory: typing.Any | _NothingType = NOTHING,
74
+ type: type | _NothingType = NOTHING,
69
75
  doc: str | None = None,
70
76
  init: bool = True,
71
77
  repr: bool = True,
72
78
  compare: bool = True,
79
+ iter: bool = True,
73
80
  kw_only: bool = False,
74
81
  in_dict: bool = True,
75
82
  exclude_field: bool = False,
76
83
  ) -> None: ...
77
84
 
78
85
  def __repr__(self) -> str: ...
79
- @typing.overload
80
- def __eq__(self, other: Attribute) -> bool: ...
81
- def __eq__(self, other: object) -> NotImplemented: ...
82
-
86
+ def __eq__(self, other: Attribute | object) -> bool: ...
83
87
  def validate_field(self) -> None: ...
84
88
 
85
89
  def attribute(
86
90
  *,
87
- default: typing.Any | NOTHING = NOTHING,
88
- default_factory: typing.Any | NOTHING = NOTHING,
89
- type: type | NOTHING = NOTHING,
91
+ default: typing.Any | _NothingType = NOTHING,
92
+ default_factory: typing.Any | _NothingType = NOTHING,
93
+ type: type | _NothingType = NOTHING,
90
94
  doc: str | None = None,
91
95
  init: bool = True,
92
96
  repr: bool = True,
93
97
  compare: bool = True,
98
+ iter: bool = True,
94
99
  kw_only: bool = False,
95
100
  in_dict: bool = True,
96
101
  exclude_field: bool = False,
@@ -112,9 +117,14 @@ def _make_prefab(
112
117
  recursive_repr: bool = False,
113
118
  ) -> type: ...
114
119
 
115
- @typing.dataclass_transform
120
+ _T = typing.TypeVar("_T")
121
+
122
+
123
+ # For some reason PyCharm can't see 'attribute'?!?
124
+ # noinspection PyUnresolvedReferences
125
+ @dataclass_transform(field_specifiers=(Attribute, attribute))
116
126
  def prefab(
117
- cls: type | None = None,
127
+ cls: type[_T] | None = None,
118
128
  *,
119
129
  init: bool = True,
120
130
  repr: bool = True,
@@ -125,7 +135,7 @@ def prefab(
125
135
  frozen: bool = False,
126
136
  dict_method: bool = False,
127
137
  recursive_repr: bool = False,
128
- ) -> type | Callable[[type], type]: ...
138
+ ) -> type[_T] | Callable[[type[_T]], type[_T]]: ...
129
139
 
130
140
  def build_prefab(
131
141
  class_name: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -44,6 +44,7 @@ Requires-Dist: sphinx-rtd-theme ; extra == 'docs'
44
44
  Provides-Extra: testing
45
45
  Requires-Dist: pytest ; extra == 'testing'
46
46
  Requires-Dist: pytest-cov ; extra == 'testing'
47
+ Requires-Dist: mypy ; extra == 'testing'
47
48
 
48
49
  # Ducktools: Class Builder #
49
50
 
@@ -53,19 +54,74 @@ of writing... functions... that will bring back the **joy** of writing classes.
53
54
  Maybe.
54
55
 
55
56
  While `attrs` and `dataclasses` are class boilerplate generators,
56
- `ducktools.classbuilder` is intended to be a dataclasses-like generator.
57
+ `ducktools.classbuilder` is intended to be a `@dataclass`-like generator.
57
58
  The goal is to handle some of the basic functions and to allow for flexible
58
59
  customization of both the field collection and the method generation.
59
60
 
60
61
  `ducktools.classbuilder.prefab` includes a prebuilt implementation using these tools.
61
62
 
63
+ Install from PyPI with:
64
+ `python -m pip install ducktools-classbuilder`
65
+
66
+ ## Usage: building a class decorator ##
67
+
68
+ In order to create a class decorator using `ducktools.classbuilder` there are
69
+ a few things you need to prepare.
70
+
71
+ 1. A field gathering function to analyse the class and collect valid `Field`s.
72
+ * An example `slot_gatherer` is included.
73
+ 2. Code generators that can make use of the gathered `Field`s to create magic method
74
+ source code.
75
+ * Example `init_maker`, `repr_maker` and `eq_maker` generators are included.
76
+ 3. A function that calls the `builder` function to apply both of these steps.
77
+
78
+ A field gathering function needs to take the original class as an argument and
79
+ return a dictionary of `{key: Field(...)}` pairs.
80
+
81
+ > [!NOTE]
82
+ > The `builder` will handle inheritance so do not collect fields from parent classes.
83
+
84
+ The code generators take the class as the only argument and return a tuple
85
+ of method source code and globals to be provided to `exec(code, globs)` in order
86
+ to generate the actual method.
87
+
88
+ The provided `slot_gatherer` looks for `__slots__` being assigned a `SlotFields`
89
+ class[^1] where keyword arguments define the names and values for the fields.
90
+
91
+ Code generator functions need to be converted to descriptors before being used.
92
+ This is done using the provided `MethodMaker` descriptor class.
93
+ ex: `init_desc = MethodMaker("__init__", init_maker)`
94
+
95
+ These parts can then be used to make a basic class boilerplate generator by
96
+ providing them to the `builder` function.
97
+
98
+ ```python
99
+ from ducktools.classbuilder import (
100
+ builder,
101
+ slot_gatherer,
102
+ init_maker, eq_maker, repr_maker,
103
+ MethodMaker,
104
+ )
105
+
106
+ init_desc = MethodMaker("__init__", init_maker)
107
+ repr_desc = MethodMaker("__repr__", repr_maker)
108
+ eq_desc = MethodMaker("__eq__", eq_maker)
109
+
110
+ def slotclass(cls):
111
+ return builder(cls, gatherer=slot_gatherer, methods={init_desc, repr_desc, eq_desc})
112
+ ```
113
+
62
114
  ## Slot Class Usage ##
63
115
 
64
- The building toolkit also includes a basic implementation that uses
65
- `__slots__` to define the fields by assigning a `SlotFields` instance.
116
+ This created `slotclass` function can then be used as a decorator to generate classes in
117
+ a similar manner to the `@dataclass` decorator from `dataclasses`.
118
+
119
+ > [!NOTE]
120
+ > `ducktools.classbuilder` includes a premade version of `slotclass` that can
121
+ > be used directly. (The included version has some extra features).
66
122
 
67
123
  ```python
68
- from ducktools.classbuilder import slotclass, Field, SlotFields
124
+ from ducktools.classbuilder import Field, SlotFields
69
125
 
70
126
  @slotclass
71
127
  class SlottedDC:
@@ -81,28 +137,36 @@ ex = SlottedDC()
81
137
  print(ex)
82
138
  ```
83
139
 
84
- ## Why does the basic implementation use slots? ##
140
+ > [!TIP]
141
+ > For more information and examples of creating class generators with additional
142
+ > features using the builder see
143
+ > [the docs](https://ducktools-classbuilder.readthedocs.io/en/latest/extension_examples.html)
144
+
145
+ ## Why does your example use `__slots__` instead of annotations? ##
85
146
 
86
- Dataclasses has a problem when you use `@dataclass(slots=True)`,
87
- although this is not unique to dataclasses but inherent to the way both
88
- `__slots__` and decorators work.
147
+ If you want to use `__slots__` in order to save memory you have to declare
148
+ them when the class is originally created as you can't add them later.
89
149
 
90
- In order for this to *appear* to work, dataclasses has to make a new class
91
- and attempt to copy over everything from the original. This is because
92
- decorators operate on classes *after they have been created* while slots
93
- need to be declared beforehand. While you can change the value of `__slots__`
94
- after a class has been created, this will have no effect on the internal
95
- structure of the class.
150
+ When you use `@dataclass(slots=True)`[^2] with `dataclasses` in order for
151
+ this to work, `dataclasses` has to make a new class and attempt to
152
+ copy over everything from the original.
153
+ This is because decorators operate on classes *after they have been created*
154
+ while slots need to be declared beforehand.
155
+ While you can change the value of `__slots__` after a class has been created,
156
+ this will have no effect on the internal structure of the class.
96
157
 
97
158
  By declaring the class using `__slots__` on the other hand, we can take
98
159
  advantage of the fact that it accepts a mapping, where the keys will be
99
160
  used as the attributes to create as slots. The values can then be used as
100
- the default values equivalently to how type hints are used in dataclasses.
161
+ the default values equivalently to how type hints are used in `dataclasses`.
101
162
 
102
- For example these two classes would be roughly equivalent, except
163
+ For example these two classes would be roughly equivalent, except that
103
164
  `@dataclass` has had to recreate the class from scratch while `@slotclass`
104
- has simply added the methods on to the original class. This is easy to
105
- demonstrate using another decorator.
165
+ has added the methods on to the original class.
166
+ This means that any references stored to the original class *before*
167
+ `@dataclass` has rebuilt the class will not be pointing towards the
168
+ correct class.
169
+ This can be demonstrated using a simple class register decorator.
106
170
 
107
171
  > This example requires Python 3.10 as earlier versions of
108
172
  > `dataclasses` did not support the `slots` argument.
@@ -140,7 +204,6 @@ print(SlotCoords())
140
204
 
141
205
  print(f"{DataCoords is class_register[DataCoords.__name__] = }")
142
206
  print(f"{SlotCoords is class_register[SlotCoords.__name__] = }")
143
-
144
207
  ```
145
208
 
146
209
  ## What features does this have? ##
@@ -158,9 +221,6 @@ field so they are present on the class if `help(...)` is called.
158
221
  If you want something with more features you can look at the `prefab.py`
159
222
  implementation which provides a 'prebuilt' implementation.
160
223
 
161
- For more information on creating class generators using the builder
162
- see [the docs](https://ducktools-classbuilder.readthedocs.io/en/latest/extension_examples.html)
163
-
164
224
  ## Will you add \<feature\> to `classbuilder.prefab`? ##
165
225
 
166
226
  No. Not unless it's something I need or find interesting.
@@ -177,3 +237,9 @@ with a specific feature, you can create or add it yourself.
177
237
  ## Credit ##
178
238
 
179
239
  Heavily inspired by [David Beazley's Cluegen](https://github.com/dabeaz/cluegen)
240
+
241
+ [^1]: `SlotFields` is actually just a subclassed `dict` with no changes. `__slots__`
242
+ works with dictionaries using the values of the keys, while fields are normally
243
+ used for documentation.
244
+
245
+ [^2]: or `@attrs.define`.
@@ -0,0 +1,10 @@
1
+ ducktools/classbuilder/__init__.py,sha256=EgrlCh0ue3DkIhafEC0YCanuCiE9t47XrIsk09GOUFk,12951
2
+ ducktools/classbuilder/__init__.pyi,sha256=Oy2c-yBT3E7UNECjpeqmnv2VJdlgu_MfIDmdjJYXIFo,2744
3
+ ducktools/classbuilder/prefab.py,sha256=KPovgZte2hfOuCOCZwQ5_3v9Y7O_vk2JpQTNG6EgJbE,29675
4
+ ducktools/classbuilder/prefab.pyi,sha256=TIT9OxYJXPbqcAiUELaWoSNIQks4pbeQy9KWYphm1YA,3897
5
+ ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
6
+ ducktools_classbuilder-0.1.1.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
7
+ ducktools_classbuilder-0.1.1.dist-info/METADATA,sha256=oCGG72wG2iT_hOr8ezdbbyinzBXf95WXmB9n9rul-Iw,9280
8
+ ducktools_classbuilder-0.1.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
9
+ ducktools_classbuilder-0.1.1.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
10
+ ducktools_classbuilder-0.1.1.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- ducktools/classbuilder/__init__.py,sha256=oEdcQdMEqaQjUfbPkHpe55iCiPssDQxf4zxoAMdsTt8,12950
2
- ducktools/classbuilder/__init__.pyi,sha256=BlpHmxIL4dAiVKgqElRR3wVs7aw73qyJRaSvr-Tuyc0,2770
3
- ducktools/classbuilder/prefab.py,sha256=qQJzN4ys6Av6s9NaabzSqRKE5BXDfhidt3EtGZ0HAxQ,29405
4
- ducktools/classbuilder/prefab.pyi,sha256=wrq8NKwy9TsQ6fpnMyTf4DaGtCOK_90NHeU61CfNOo4,3589
5
- ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
6
- ducktools_classbuilder-0.1.0.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
7
- ducktools_classbuilder-0.1.0.dist-info/METADATA,sha256=FHgeLZx-RfcvY2ty1l6O19z449So-UQZIcBinJ8n5xU,6675
8
- ducktools_classbuilder-0.1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
9
- ducktools_classbuilder-0.1.0.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
10
- ducktools_classbuilder-0.1.0.dist-info/RECORD,,