ducktools-classbuilder 0.2.0__tar.gz → 0.3.0__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 (58) hide show
  1. {ducktools_classbuilder-0.2.0/src/ducktools_classbuilder.egg-info → ducktools_classbuilder-0.3.0}/PKG-INFO +1 -1
  2. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/src/ducktools/classbuilder/__init__.py +27 -41
  3. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/src/ducktools/classbuilder/__init__.pyi +2 -3
  4. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/src/ducktools/classbuilder/prefab.py +7 -7
  5. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/src/ducktools/classbuilder/prefab.pyi +4 -4
  6. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0/src/ducktools_classbuilder.egg-info}/PKG-INFO +1 -1
  7. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/src/ducktools_classbuilder.egg-info/SOURCES.txt +1 -0
  8. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/test_funcs.py +3 -3
  9. ducktools_classbuilder-0.3.0/tests/test_core.py +319 -0
  10. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/LICENSE.md +0 -0
  11. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/MANIFEST.in +0 -0
  12. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/README.md +0 -0
  13. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/docs/Makefile +0 -0
  14. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/docs/api.md +0 -0
  15. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/docs/approach_vs_tool.md +0 -0
  16. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/docs/conf.py +0 -0
  17. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/docs/extension_examples.md +0 -0
  18. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/docs/index.md +0 -0
  19. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/docs/make.bat +0 -0
  20. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/docs/perf/performance_tests.md +0 -0
  21. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/docs/prefab/index.md +0 -0
  22. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/pyproject.toml +0 -0
  23. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/setup.cfg +0 -0
  24. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/src/ducktools/classbuilder/py.typed +0 -0
  25. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
  26. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/src/ducktools_classbuilder.egg-info/requires.txt +0 -0
  27. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
  28. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/dynamic/test_compare_attrib.py +0 -0
  29. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/dynamic/test_construction.py +0 -0
  30. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/dynamic/test_internals.py +0 -0
  31. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/dynamic/test_pre_post_init.py +0 -0
  32. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/dynamic/test_slots_novalues.py +0 -0
  33. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/dynamic/test_slotted_class.py +0 -0
  34. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/conftest.py +0 -0
  35. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/creation.py +0 -0
  36. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/creation_empty.py +0 -0
  37. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/dunders.py +0 -0
  38. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/fails/creation_1.py +0 -0
  39. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/fails/creation_2.py +0 -0
  40. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/fails/creation_3.py +0 -0
  41. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/fails/creation_5.py +0 -0
  42. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/fails/inheritance_1.py +0 -0
  43. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/fails/inheritance_2.py +0 -0
  44. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/frozen_prefabs.py +0 -0
  45. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/funcs_prefabs.py +0 -0
  46. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/hint_syntax.py +0 -0
  47. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/inheritance.py +0 -0
  48. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/init_ex.py +0 -0
  49. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/kw_only.py +0 -0
  50. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/examples/repr_func.py +0 -0
  51. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/test_creation.py +0 -0
  52. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/test_dunders.py +0 -0
  53. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/test_frozen.py +0 -0
  54. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/test_hint_syntax.py +0 -0
  55. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/test_inheritance.py +0 -0
  56. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/test_init.py +0 -0
  57. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/test_kw_only.py +0 -0
  58. {ducktools_classbuilder-0.2.0 → ducktools_classbuilder-0.3.0}/tests/prefab/shared/test_repr.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -19,34 +19,12 @@
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.2.0"
22
+ __version__ = "v0.3.0"
23
23
 
24
24
  # Change this name if you make heavy modifications
25
25
  INTERNALS_DICT = "__classbuilder_internals__"
26
26
 
27
27
 
28
- def get_internals(cls):
29
- """
30
- Utility function to get the internals dictionary
31
- or return None.
32
-
33
- As generated classes will always have 'fields'
34
- and 'local_fields' attributes this will always
35
- evaluate as 'truthy' if this is a generated class.
36
-
37
- Generally you should use the helper get_flags and
38
- get_fields methods.
39
-
40
- Usage:
41
- if internals := get_internals(cls):
42
- ...
43
-
44
- :param cls: generated class
45
- :return: internals dictionary of the class or None
46
- """
47
- return getattr(cls, INTERNALS_DICT, None)
48
-
49
-
50
28
  def get_fields(cls, *, local=False):
51
29
  """
52
30
  Utility function to gather the fields dictionary
@@ -71,7 +49,9 @@ def get_flags(cls):
71
49
  return getattr(cls, INTERNALS_DICT)["flags"]
72
50
 
73
51
 
74
- def get_inst_fields(inst):
52
+ def _get_inst_fields(inst):
53
+ # This is an internal helper for constructing new
54
+ # 'Field' instances from existing ones.
75
55
  return {
76
56
  k: getattr(inst, k)
77
57
  for k in get_fields(type(inst))
@@ -105,7 +85,7 @@ class MethodMaker:
105
85
  self.code_generator = code_generator
106
86
 
107
87
  def __repr__(self):
108
- return f"<MethodMaker for {self.funcname} method>"
88
+ return f"<MethodMaker for {self.funcname!r} method>"
109
89
 
110
90
  def __get__(self, instance, cls):
111
91
  local_vars = {}
@@ -122,7 +102,7 @@ class MethodMaker:
122
102
  return method.__get__(instance, cls)
123
103
 
124
104
 
125
- def init_maker(cls, *, null=NOTHING):
105
+ def init_maker(cls, *, null=NOTHING, extra_code=None):
126
106
  fields = get_fields(cls)
127
107
  flags = get_flags(cls)
128
108
 
@@ -150,8 +130,17 @@ def init_maker(cls, *, null=NOTHING):
150
130
  assignments.append(assignment)
151
131
 
152
132
  args = ", ".join(arglist)
153
- assigns = "\n ".join(assignments)
154
- code = f"def __init__(self, {args}):\n" f" {assigns}\n"
133
+ assigns = "\n ".join(assignments) if assignments else "pass\n"
134
+ code = (
135
+ f"def __init__(self, {args}):\n"
136
+ f" {assigns}\n"
137
+ )
138
+ # Handle additional function calls
139
+ # Used for validate_field on fieldclasses
140
+ if extra_code:
141
+ for line in extra_code:
142
+ code += f" {line}\n"
143
+
155
144
  return code, globs
156
145
 
157
146
 
@@ -300,7 +289,7 @@ class Field:
300
289
  :param kwargs: Additional keyword arguments for subclasses
301
290
  :return: new field subclass instance
302
291
  """
303
- argument_dict = {**get_inst_fields(fld), **kwargs}
292
+ argument_dict = {**_get_inst_fields(fld), **kwargs}
304
293
 
305
294
  return cls(**argument_dict)
306
295
 
@@ -407,6 +396,14 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
407
396
  return cls
408
397
 
409
398
 
399
+ def _field_init_func(cls):
400
+ # Fields need a different Nothing for their __init__ generation
401
+ # And an extra call to validate_field
402
+ field_nothing = _NothingType()
403
+ extra_calls = ["self.validate_field()"]
404
+ return init_maker(cls, null=field_nothing, extra_code=extra_calls)
405
+
406
+
410
407
  def fieldclass(cls):
411
408
  """
412
409
  This is a special decorator for making Field subclasses using __slots__.
@@ -417,18 +414,7 @@ def fieldclass(cls):
417
414
  :return: Modified subclass
418
415
  """
419
416
 
420
- # Fields need a way to call their validate method
421
- # So append it to the code from __init__.
422
- def field_init_func(cls_):
423
- code, globs = init_maker(cls_, null=field_nothing)
424
- code += " self.validate_field()\n"
425
- return code, globs
426
-
427
- field_nothing = _NothingType()
428
- field_init_desc = MethodMaker(
429
- "__init__",
430
- field_init_func,
431
- )
417
+ field_init_desc = MethodMaker("__init__", _field_init_func)
432
418
  field_methods = frozenset({field_init_desc, repr_desc, eq_desc})
433
419
 
434
420
  cls = builder(
@@ -6,13 +6,11 @@ _py_type = type # Alias for type where it is used as a name
6
6
  __version__: str
7
7
  INTERNALS_DICT: str
8
8
 
9
- def get_internals(cls) -> dict[str, typing.Any] | None: ...
10
-
11
9
  def get_fields(cls: type, *, local: bool = False) -> dict[str, Field]: ...
12
10
 
13
11
  def get_flags(cls:type) -> dict[str, bool]: ...
14
12
 
15
- def get_inst_fields(inst: typing.Any) -> dict[str, typing.Any]: ...
13
+ def _get_inst_fields(inst: typing.Any) -> dict[str, typing.Any]: ...
16
14
 
17
15
  class _NothingType:
18
16
  ...
@@ -32,6 +30,7 @@ def init_maker(
32
30
  cls: type,
33
31
  *,
34
32
  null: _NothingType = NOTHING,
33
+ extra_code: None | list[str] = None
35
34
  ) -> tuple[str, dict[str, typing.Any]]: ...
36
35
  def repr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
37
36
  def eq_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
@@ -398,7 +398,7 @@ def get_asdict_maker():
398
398
  vals = ", ".join(
399
399
  f"'{name}': self.{name}"
400
400
  for name, attrib in fields.items()
401
- if attrib.in_dict and not attrib.exclude_field
401
+ if attrib.serialize and not attrib.exclude_field
402
402
  )
403
403
  out_dict = f"{{{vals}}}"
404
404
  code = f"def as_dict(self): return {out_dict}"
@@ -428,7 +428,7 @@ class Attribute(Field):
428
428
  compare=True,
429
429
  iter=True,
430
430
  kw_only=False,
431
- in_dict=True,
431
+ serialize=True,
432
432
  exclude_field=False,
433
433
  )
434
434
 
@@ -450,7 +450,7 @@ def attribute(
450
450
  compare=True,
451
451
  iter=True,
452
452
  kw_only=False,
453
- in_dict=True,
453
+ serialize=True,
454
454
  exclude_field=False,
455
455
  doc=None,
456
456
  type=NOTHING,
@@ -466,7 +466,7 @@ def attribute(
466
466
  :param compare: Include this attribute in the class __eq__
467
467
  :param iter: Include this attribute in the class __iter__ if generated
468
468
  :param kw_only: Make this argument keyword only in init
469
- :param in_dict: Include this attribute in methods that serialise to dict
469
+ :param serialize: Include this attribute in methods that serialize to dict
470
470
  :param exclude_field: Exclude this field from all magic method generation
471
471
  apart from __init__ signature
472
472
  and do not include it in PREFAB_FIELDS
@@ -484,7 +484,7 @@ def attribute(
484
484
  compare=compare,
485
485
  iter=iter,
486
486
  kw_only=kw_only,
487
- in_dict=in_dict,
487
+ serialize=serialize,
488
488
  exclude_field=exclude_field,
489
489
  doc=doc,
490
490
  type=type,
@@ -906,7 +906,7 @@ def is_prefab_instance(o):
906
906
 
907
907
  def as_dict(o):
908
908
  """
909
- Get the valid fields from a prefab respecting the in_dict
909
+ Get the valid fields from a prefab respecting the serialize
910
910
  values of attributes
911
911
 
912
912
  :param o: instance of a prefab class
@@ -927,5 +927,5 @@ def as_dict(o):
927
927
  return {
928
928
  name: getattr(o, name)
929
929
  for name, attrib in flds.items()
930
- if attrib.in_dict and not attrib.exclude_field
930
+ if attrib.serialize and not attrib.exclude_field
931
931
  }
@@ -6,7 +6,7 @@ from collections.abc import Callable
6
6
  from . import (
7
7
  INTERNALS_DICT, NOTHING,
8
8
  Field, MethodMaker, SlotFields as SlotFields,
9
- builder, fieldclass, get_internals, slot_gatherer
9
+ builder, fieldclass, get_flags, get_fields, slot_gatherer
10
10
  )
11
11
 
12
12
  # noinspection PyUnresolvedReferences
@@ -63,7 +63,7 @@ class Attribute(Field):
63
63
  compare: bool
64
64
  iter: bool
65
65
  kw_only: bool
66
- in_dict: bool
66
+ serialize: bool
67
67
  exclude_field: bool
68
68
 
69
69
  def __init__(
@@ -78,7 +78,7 @@ class Attribute(Field):
78
78
  compare: bool = True,
79
79
  iter: bool = True,
80
80
  kw_only: bool = False,
81
- in_dict: bool = True,
81
+ serialize: bool = True,
82
82
  exclude_field: bool = False,
83
83
  ) -> None: ...
84
84
 
@@ -97,7 +97,7 @@ def attribute(
97
97
  compare: bool = True,
98
98
  iter: bool = True,
99
99
  kw_only: bool = False,
100
- in_dict: bool = True,
100
+ serialize: bool = True,
101
101
  exclude_field: bool = False,
102
102
  ) -> Attribute: ...
103
103
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -21,6 +21,7 @@ src/ducktools_classbuilder.egg-info/SOURCES.txt
21
21
  src/ducktools_classbuilder.egg-info/dependency_links.txt
22
22
  src/ducktools_classbuilder.egg-info/requires.txt
23
23
  src/ducktools_classbuilder.egg-info/top_level.txt
24
+ tests/test_core.py
24
25
  tests/prefab/dynamic/test_compare_attrib.py
25
26
  tests/prefab/dynamic/test_construction.py
26
27
  tests/prefab/dynamic/test_internals.py
@@ -45,18 +45,18 @@ def test_as_dict_excludes():
45
45
  @prefab
46
46
  class ExcludesUncached:
47
47
  name: str
48
- password: str = attribute(in_dict=False)
48
+ password: str = attribute(serialize=False)
49
49
 
50
50
  @prefab(dict_method=True)
51
51
  class ExcludesCached:
52
52
  name: str
53
- password: str = attribute(in_dict=False)
53
+ password: str = attribute(serialize=False)
54
54
 
55
55
  @prefab(dict_method=True)
56
56
  class ExcludesSlots:
57
57
  __slots__ = SlotFields(
58
58
  name=attribute(type=str),
59
- password=attribute(in_dict=False, type=str)
59
+ password=attribute(serialize=False, type=str)
60
60
  )
61
61
 
62
62
  @prefab(dict_method=True)
@@ -0,0 +1,319 @@
1
+ # Tests for the core 'builder'
2
+ import pytest
3
+
4
+ from ducktools.classbuilder import (
5
+ INTERNALS_DICT,
6
+ NOTHING,
7
+ get_fields,
8
+ get_flags,
9
+ MethodMaker,
10
+ init_desc,
11
+ builder,
12
+ Field,
13
+ SlotFields,
14
+ slot_gatherer,
15
+ slotclass,
16
+ fieldclass,
17
+ )
18
+
19
+
20
+ def test_get_fields_flags():
21
+ local_fields = {"Example": Field()}
22
+ resolved_fields = {"ParentField": Field(), "Example": Field()}
23
+ flags = {"slotted": False}
24
+
25
+ internals_dict = {
26
+ "fields": resolved_fields,
27
+ "local_fields": local_fields,
28
+ "flags": flags,
29
+ }
30
+
31
+ class ExampleFields:
32
+ ...
33
+
34
+ setattr(ExampleFields, INTERNALS_DICT, internals_dict)
35
+
36
+ assert get_fields(ExampleFields) == resolved_fields
37
+ assert get_fields(ExampleFields, local=True) == local_fields
38
+ assert get_flags(ExampleFields) == flags
39
+
40
+
41
+ def test_method_maker():
42
+ def generator(cls):
43
+ code = "def demo(self): return self.x"
44
+ globs = {}
45
+ return code, globs
46
+
47
+ method_desc = MethodMaker("demo", generator)
48
+
49
+ assert repr(method_desc) == "<MethodMaker for 'demo' method>"
50
+
51
+ class ValueX:
52
+ demo = method_desc
53
+
54
+ def __init__(self):
55
+ self.x = "Example Value"
56
+
57
+ ex = ValueX()
58
+
59
+ assert ValueX.__dict__["demo"] == method_desc
60
+
61
+ assert ex.x == "Example Value"
62
+ assert ex.demo() == "Example Value"
63
+
64
+ # Should no longer be equal as demo was called
65
+ assert ValueX.__dict__["demo"] != method_desc
66
+
67
+
68
+ def test_construct_field():
69
+ f = Field()
70
+ assert f.default is NOTHING
71
+ assert f.default_factory is NOTHING
72
+ assert f.type is NOTHING
73
+ assert f.doc is None
74
+
75
+ with pytest.raises(AttributeError):
76
+ Field(default=None, default_factory=list)
77
+
78
+
79
+ def test_eq_field():
80
+ f1 = Field(default=True)
81
+ f2 = Field(default=False)
82
+ f3 = Field(default_factory=list)
83
+ f4 = Field(default=True, type=bool)
84
+ f5 = Field(default=True, doc="True or False")
85
+
86
+ assert f1 != f2
87
+ assert f1 != f3
88
+ assert f1 != f4
89
+ assert f1 != f5
90
+
91
+ f1r = Field(default=True)
92
+ assert f1 == f1r
93
+
94
+
95
+ def test_from_field():
96
+ f1 = Field(default=True)
97
+ f2 = Field(default=False)
98
+ f3 = Field(default_factory=list)
99
+ f4 = Field(default=True, type=bool)
100
+ f5 = Field(default=True, doc="True or False")
101
+
102
+ for fld in [f1, f2, f3, f4, f5]:
103
+ assert fld == Field.from_field(fld)
104
+
105
+
106
+ def test_repr_field():
107
+ f1 = Field(default=True)
108
+ f2 = Field(default=False)
109
+ f3 = Field(default_factory=list)
110
+ f4 = Field(default=True, type=bool)
111
+ f5 = Field(default=True, doc="True or False")
112
+
113
+ nothing_repr = repr(NOTHING)
114
+
115
+ f1_repr = f"Field(default=True, default_factory={nothing_repr}, type={nothing_repr}, doc=None)"
116
+ f2_repr = f"Field(default=False, default_factory={nothing_repr}, type={nothing_repr}, doc=None)"
117
+ f3_repr = f"Field(default={nothing_repr}, default_factory=<class 'list'>, type={nothing_repr}, doc=None)"
118
+ f4_repr = f"Field(default=True, default_factory={nothing_repr}, type=<class 'bool'>, doc=None)"
119
+ f5_repr = f"Field(default=True, default_factory={nothing_repr}, type={nothing_repr}, doc='True or False')"
120
+
121
+ assert repr(f1) == f1_repr
122
+ assert repr(f2) == f2_repr
123
+ assert repr(f3) == f3_repr
124
+ assert repr(f4) == f4_repr
125
+ assert repr(f5) == f5_repr
126
+
127
+
128
+ def test_slot_gatherer_success():
129
+
130
+ fields = {
131
+ "a": Field(default=1),
132
+ "b": Field(default=2),
133
+ "c": Field(default_factory=list, doc="a list"),
134
+ "d": Field(type=str)
135
+ }
136
+
137
+ class SlotsExample:
138
+ __slots__ = SlotFields(
139
+ a=1,
140
+ b=Field(default=2),
141
+ c=Field(default_factory=list, doc="a list"),
142
+ d=Field(type=str),
143
+ )
144
+
145
+ slots = slot_gatherer(SlotsExample)
146
+
147
+ assert slots == fields
148
+ assert SlotsExample.__slots__ == {"a": None, "b": None, "c": "a list", "d": None}
149
+ assert SlotsExample.__annotations__ == {"d": str}
150
+
151
+
152
+ def test_slot_gatherer_failure():
153
+ class NoSlots:
154
+ ...
155
+
156
+ with pytest.raises(TypeError):
157
+ slot_gatherer(NoSlots)
158
+
159
+ class WrongSlots:
160
+ __slots__ = ["a", "b", "c"]
161
+
162
+ with pytest.raises(TypeError):
163
+ slot_gatherer(WrongSlots)
164
+
165
+ class DictSlots:
166
+ __slots__ = {"a": "documentation"}
167
+
168
+ with pytest.raises(TypeError):
169
+ slot_gatherer(DictSlots)
170
+
171
+
172
+ def test_slotclass_empty():
173
+ @slotclass
174
+ class SlotClass:
175
+ __slots__ = SlotFields()
176
+
177
+ ex = SlotClass()
178
+ ex2 = SlotClass()
179
+
180
+ assert repr(ex) == "test_slotclass_empty.<locals>.SlotClass()"
181
+ assert ex == ex2
182
+
183
+
184
+ def test_slotclass_methods():
185
+
186
+ class SlotClass:
187
+ __slots__ = SlotFields()
188
+
189
+ assert "__init__" not in SlotClass.__dict__
190
+ assert "__repr__" not in SlotClass.__dict__
191
+ assert "__eq__" not in SlotClass.__dict__
192
+
193
+ SlotClass = slotclass(SlotClass)
194
+
195
+ assert "__init__" in SlotClass.__dict__
196
+ assert "__repr__" in SlotClass.__dict__
197
+ assert "__eq__" in SlotClass.__dict__
198
+
199
+
200
+ def test_slotclass_attributes():
201
+ @slotclass
202
+ class SlotClass:
203
+ __slots__ = SlotFields(
204
+ a=1,
205
+ b=Field(default=2, type=int),
206
+ c=Field(default_factory=list, doc="a list"),
207
+ )
208
+
209
+ prefix = "test_slotclass_attributes.<locals>."
210
+
211
+ ex = SlotClass()
212
+ ex2 = SlotClass()
213
+ ex3 = SlotClass(c=[1, 2, 3])
214
+ ex4 = SlotClass(4, 5, [1, 2, 3])
215
+
216
+ assert ex.a == 1
217
+ assert ex.b == 2
218
+ assert ex.c == []
219
+
220
+ assert ex3.c == [1, 2, 3]
221
+
222
+ assert ex4.a == 4
223
+ assert ex4.b == 5
224
+ assert ex4.c == [1, 2, 3]
225
+
226
+ assert ex == ex2
227
+ assert ex != ex3
228
+
229
+ assert repr(ex) == f"{prefix}SlotClass(a=1, b=2, c=[])"
230
+ assert repr(ex3) == f"{prefix}SlotClass(a=1, b=2, c=[1, 2, 3])"
231
+
232
+
233
+ def test_slotclass_nodefault():
234
+ @slotclass
235
+ class SlotClass:
236
+ __slots__ = SlotFields(
237
+ a=Field(),
238
+ b=2,
239
+ c=Field(default_factory=list, doc="a list"),
240
+ )
241
+
242
+ ex = SlotClass(1)
243
+ ex2 = SlotClass(a=2, b=4, c=[8, 16, 32])
244
+
245
+ assert ex.a == 1
246
+ assert ex.b == 2
247
+ assert ex.c == []
248
+
249
+ assert ex2.a == 2
250
+ assert ex2.b == 4
251
+ assert ex2.c == [8, 16, 32]
252
+
253
+
254
+ def test_slotclass_ordering():
255
+ with pytest.raises(SyntaxError):
256
+ # Non-default argument after default
257
+ @slotclass
258
+ class OrderingError:
259
+ __slots__ = SlotFields(
260
+ x=1,
261
+ y=Field(),
262
+ )
263
+
264
+
265
+ def test_slotclass_norepr_noeq():
266
+ @slotclass(methods={init_desc})
267
+ class SlotClass:
268
+ __slots__ = SlotFields(
269
+ a=Field(),
270
+ b=2,
271
+ c=Field(default_factory=list, doc="a list"),
272
+ )
273
+
274
+ assert "__repr__" not in SlotClass.__dict__
275
+ assert "__eq__" not in SlotClass.__dict__
276
+
277
+
278
+ def test_fieldclass():
279
+ @fieldclass
280
+ class NewField(Field):
281
+ __slots__ = SlotFields(serialize=True)
282
+
283
+ f = NewField()
284
+
285
+ assert f.default is NOTHING
286
+ assert f.default_factory is NOTHING
287
+ assert f.type is NOTHING
288
+ assert f.doc is None
289
+ assert f.serialize is True
290
+
291
+ f2 = NewField(default=1, serialize=False)
292
+
293
+ assert f2.default == 1
294
+ assert f2.serialize is False
295
+
296
+ with pytest.raises(TypeError):
297
+ # All arguments are keyword only in fieldclasses
298
+ NewField(42)
299
+
300
+
301
+ def test_builder_noclass():
302
+ mini_slotclass = builder(gatherer=slot_gatherer, methods={init_desc})
303
+
304
+ @mini_slotclass
305
+ class SlotClass:
306
+ __slots__ = SlotFields(
307
+ a=Field(),
308
+ b=2,
309
+ c=Field(default_factory=list, doc="a list"),
310
+ )
311
+
312
+ assert "__init__" in SlotClass.__dict__
313
+ assert "__repr__" not in SlotClass.__dict__
314
+ assert "__eq__" not in SlotClass.__dict__
315
+
316
+ x = SlotClass(12)
317
+ assert x.a == 12
318
+ assert x.b == 2
319
+ assert x.c == []