ducktools-classbuilder 0.6.3__py3-none-any.whl → 0.7.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.

@@ -30,11 +30,10 @@
30
30
  # but is also the metaclass used to construct 'Field'.
31
31
  # Field itself sidesteps this by defining __slots__ to avoid that branch.
32
32
 
33
- import sys
33
+ import os
34
34
 
35
35
  from .annotations import get_ns_annotations, is_classvar
36
-
37
- __version__ = "v0.6.3"
36
+ from ._version import __version__, __version_tuple__
38
37
 
39
38
  # Change this name if you make heavy modifications
40
39
  INTERNALS_DICT = "__classbuilder_internals__"
@@ -43,7 +42,7 @@ META_GATHERER_NAME = "_meta_gatherer"
43
42
 
44
43
  # If testing, make Field classes frozen to make sure attributes are not
45
44
  # overwritten. When running this is a performance penalty so it is not required.
46
- _UNDER_TESTING = "pytest" in sys.modules
45
+ _UNDER_TESTING = os.environ.get("PYTEST_VERSION") is not None
47
46
 
48
47
  # Obtain types the same way types.py does in pypy
49
48
  # See: https://github.com/pypy/pypy/blob/19d9fa6be11165116dd0839b9144d969ab426ae7/lib-python/3/types.py#L61-L73
@@ -708,10 +707,6 @@ def make_slot_gatherer(field_type=Field):
708
707
  "in order to generate a slotclass"
709
708
  )
710
709
 
711
- # Don't want to mutate original annotations so make a copy if it exists
712
- # Looking at the dict is a Python3.9 or earlier requirement
713
- cls_annotations = get_ns_annotations(cls_dict)
714
-
715
710
  cls_fields = {}
716
711
  slot_replacement = {}
717
712
 
@@ -725,8 +720,6 @@ def make_slot_gatherer(field_type=Field):
725
720
 
726
721
  if isinstance(v, field_type):
727
722
  attrib = v
728
- if attrib.type is not NOTHING:
729
- cls_annotations[k] = attrib.type
730
723
  else:
731
724
  # Plain values treated as defaults
732
725
  attrib = field_type(default=v)
@@ -739,7 +732,6 @@ def make_slot_gatherer(field_type=Field):
739
732
  # In this case, slots with documentation and new annotations.
740
733
  modifications = {
741
734
  "__slots__": slot_replacement,
742
- "__annotations__": cls_annotations,
743
735
  }
744
736
 
745
737
  return cls_fields, modifications
@@ -11,6 +11,7 @@ _py_type = type | str # Alias for type hint values
11
11
  _CopiableMappings = dict[str, typing.Any] | MappingProxyType[str, typing.Any]
12
12
 
13
13
  __version__: str
14
+ __version_tuple__: tuple[str | int, ...]
14
15
  INTERNALS_DICT: str
15
16
  META_GATHERER_NAME: str
16
17
 
@@ -0,0 +1,2 @@
1
+ __version__ = "0.7.1"
2
+ __version_tuple__ = (0, 7, 1)
@@ -24,6 +24,19 @@ import sys
24
24
  import builtins
25
25
 
26
26
 
27
+ # Evil stuff from types.py
28
+ def _cell_factory():
29
+ a = 1
30
+
31
+ def f():
32
+ nonlocal a
33
+ return f.__closure__[0]
34
+ _FunctionType = type(_cell_factory)
35
+ _CellType = type(_cell_factory())
36
+ del _cell_factory
37
+ # End evil stuff from types.py
38
+
39
+
27
40
  class _StringGlobs(dict):
28
41
  """
29
42
  Based on the fake globals dictionary used for annotations
@@ -99,6 +112,62 @@ def eval_hint(hint, context=None, *, recursion_limit=2):
99
112
  return hint
100
113
 
101
114
 
115
+ def call_annotate_func(annotate):
116
+ # Python 3.14 breaks the old methods of getting annotations
117
+ # The new annotationlib currently relies on 'ast' and 'functools'
118
+ # that this project tries to avoid importing.
119
+
120
+ # The basic logic is copied from there, however, replacing ForwardRef
121
+ # with a more basic class.
122
+ # While `annotationlib` is trying to return ForwardRef objects that can
123
+ # be evaluated later, this module only cares about annotations that can
124
+ # be evaluated at the point this function is called.
125
+ # As such we throw away the other information and just return strings
126
+ # instead of forwardrefs.
127
+
128
+ try:
129
+ raw_annotations = annotate(1)
130
+ except NameError:
131
+ pass
132
+ else:
133
+ return raw_annotations
134
+
135
+ # The annotate func may support forwardref natively
136
+ try:
137
+ raw_annotations = annotate(2)
138
+ except NotImplementedError:
139
+ pass
140
+ else:
141
+ return raw_annotations
142
+
143
+ # Not supported so we have to implement our own deferred handling
144
+ # Some modified logic from annotationlib
145
+ namespace = {**annotate.__builtins__, **annotate.__globals__}
146
+ globs = _StringGlobs(namespace)
147
+
148
+ # This handles closures where the variable is defined after get annotations is called.
149
+ if annotate.__closure__:
150
+ freevars = annotate.__code__.co_freevars
151
+ new_closure = []
152
+ for i, cell in enumerate(annotate.__closure__):
153
+ try:
154
+ cell.cell_contents
155
+ except ValueError:
156
+ if i < len(freevars):
157
+ name = freevars[i]
158
+ else:
159
+ name = "__cell__"
160
+ new_closure.append(_CellType(name))
161
+ else:
162
+ new_closure.append(cell)
163
+ closure = tuple(new_closure)
164
+ else:
165
+ closure = None
166
+
167
+ new_annotate = _FunctionType(annotate.__code__, globs, closure=closure)
168
+ return new_annotate(1)
169
+
170
+
102
171
  def get_ns_annotations(ns, eval_str=True):
103
172
  """
104
173
  Given a class namespace, attempt to retrieve the
@@ -112,36 +181,50 @@ def get_ns_annotations(ns, eval_str=True):
112
181
  :param eval_str: Attempt to evaluate string annotations (default to True)
113
182
  :return: dictionary of evaluated annotations
114
183
  """
115
- raw_annotations = ns.get("__annotations__", {})
116
184
 
117
- if not eval_str:
118
- return raw_annotations.copy()
185
+ # In 3.14 the 'canonical' method of getting annotations is to use __annotate__
186
+ # If this doesn't exist, check __annotations__ and treat as 3.13 or earlier.
187
+ annotate = ns.get("__annotate__")
119
188
 
120
- try:
121
- obj_modulename = ns["__module__"]
122
- except KeyError:
123
- obj_module = None
189
+ if annotate is not None:
190
+ raw_annotations = call_annotate_func(annotate)
124
191
  else:
125
- obj_module = sys.modules.get(obj_modulename, None)
192
+ raw_annotations = ns.get("__annotations__", {})
126
193
 
127
- if obj_module:
128
- obj_globals = vars(obj_module)
129
- else:
130
- obj_globals = {}
194
+ # Unlike annotationlib we still try to evaluate string annotations
195
+ # This will catch cases where someone has used a literal string for a
196
+ # single attribute.
197
+ if eval_str:
198
+ try:
199
+ obj_modulename = ns["__module__"]
200
+ except KeyError:
201
+ obj_module = None
202
+ else:
203
+ obj_module = sys.modules.get(obj_modulename, None)
204
+
205
+ if obj_module:
206
+ obj_globals = vars(obj_module)
207
+ else:
208
+ obj_globals = {}
131
209
 
132
- # Type parameters should be usable in hints without breaking
133
- # This is for Python 3.12+
134
- type_params = {
135
- repr(param): param
136
- for param in ns.get("__type_params__", ())
137
- }
210
+ # Type parameters should be usable in hints without breaking
211
+ # This is for Python 3.12+
212
+ type_params = {
213
+ repr(param): param
214
+ for param in ns.get("__type_params__", ())
215
+ }
138
216
 
139
- context = {**vars(builtins), **obj_globals, **type_params, **ns}
217
+ context = {**vars(builtins), **obj_globals, **type_params, **ns}
218
+
219
+ annotations = {
220
+ k: eval_hint(v, context)
221
+ for k, v in raw_annotations.items()
222
+ }
223
+
224
+ else:
225
+ annotations = raw_annotations.copy()
140
226
 
141
- return {
142
- k: eval_hint(v, context)
143
- for k, v in raw_annotations.items()
144
- }
227
+ return annotations
145
228
 
146
229
 
147
230
  def is_classvar(hint):
@@ -1,3 +1,4 @@
1
+ from collections.abc import Callable
1
2
  import typing
2
3
  import types
3
4
 
@@ -8,6 +9,11 @@ class _StringGlobs(dict):
8
9
  def __missing__(self, key: _T) -> _T: ...
9
10
 
10
11
 
12
+ def call_annotate_func(
13
+ annotate: Callable[[int], dict[str, type | typing.ForwardRef]]
14
+ ) -> dict[str, type | str]: ...
15
+
16
+
11
17
  def eval_hint(
12
18
  hint: type | str,
13
19
  context: None | dict[str, typing.Any] = None,
@@ -241,12 +241,24 @@ def as_dict_generator(cls, funcname="as_dict"):
241
241
  if attrib.serialize
242
242
  )
243
243
  out_dict = f"{{{vals}}}"
244
- code = f"def {funcname}(self): return {out_dict}"
244
+ code = f"def {funcname}(self): return {out_dict}\n"
245
245
 
246
246
  globs = {}
247
247
  return GeneratedCode(code, globs)
248
248
 
249
249
 
250
+ def hash_generator(cls, funcname="__hash__"):
251
+ fields = get_attributes(cls)
252
+ vals = ", ".join(
253
+ f"self.{name}"
254
+ for name, attrib in fields.items()
255
+ if attrib.compare
256
+ )
257
+ code = f"def {funcname}(self): return hash(({vals}))\n"
258
+ globs = {}
259
+ return GeneratedCode(code, globs)
260
+
261
+
250
262
  init_maker = MethodMaker("__init__", init_generator)
251
263
  prefab_init_maker = MethodMaker(PREFAB_INIT_FUNC, init_generator)
252
264
  repr_maker = MethodMaker(
@@ -259,6 +271,7 @@ recursive_repr_maker = MethodMaker(
259
271
  )
260
272
  iter_maker = MethodMaker("__iter__", iter_generator)
261
273
  asdict_maker = MethodMaker("as_dict", as_dict_generator)
274
+ hash_maker = MethodMaker("__hash__", hash_generator)
262
275
 
263
276
 
264
277
  # Updated field with additional attributes
@@ -419,6 +432,7 @@ def _make_prefab(
419
432
  if frozen:
420
433
  methods.add(frozen_setattr_maker)
421
434
  methods.add(frozen_delattr_maker)
435
+ methods.add(hash_maker)
422
436
  if dict_method:
423
437
  methods.add(asdict_maker)
424
438
 
@@ -33,6 +33,7 @@ def get_attributes(cls: type) -> dict[str, Attribute]: ...
33
33
  def init_generator(cls: type, funcname: str = "__init__") -> GeneratedCode: ...
34
34
  def iter_generator(cls: type, funcname: str = "__iter__") -> GeneratedCode: ...
35
35
  def as_dict_generator(cls: type, funcname: str = "as_dict") -> GeneratedCode: ...
36
+ def hash_generator(cls: type, funcname: str = "__hash__") -> GeneratedCode: ...
36
37
 
37
38
  init_maker: MethodMaker
38
39
  prefab_init_maker: MethodMaker
@@ -41,6 +42,7 @@ recursive_repr_maker: MethodMaker
41
42
  eq_maker: MethodMaker
42
43
  iter_maker: MethodMaker
43
44
  asdict_maker: MethodMaker
45
+ hash_maker: MethodMaker
44
46
 
45
47
  class Attribute(Field):
46
48
  __slots__: dict
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.6.3
3
+ Version: 0.7.1
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -32,6 +32,7 @@ Classifier: Programming Language :: Python :: 3.9
32
32
  Classifier: Programming Language :: Python :: 3.10
33
33
  Classifier: Programming Language :: Python :: 3.11
34
34
  Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: Programming Language :: Python :: 3.13
35
36
  Classifier: Operating System :: OS Independent
36
37
  Classifier: License :: OSI Approved :: MIT License
37
38
  Requires-Python: >=3.8
@@ -45,7 +46,7 @@ Provides-Extra: performance_tests
45
46
  Requires-Dist: attrs ; extra == 'performance_tests'
46
47
  Requires-Dist: pydantic ; extra == 'performance_tests'
47
48
  Provides-Extra: testing
48
- Requires-Dist: pytest ; extra == 'testing'
49
+ Requires-Dist: pytest >=8.2 ; extra == 'testing'
49
50
  Requires-Dist: pytest-cov ; extra == 'testing'
50
51
  Requires-Dist: mypy ; extra == 'testing'
51
52
  Requires-Dist: typing-extensions ; extra == 'testing'
@@ -0,0 +1,13 @@
1
+ ducktools/classbuilder/__init__.py,sha256=oT-U127MZ2xVXTaaCmH02Mxh1JhnQ65pkydcd2otqhc,32517
2
+ ducktools/classbuilder/__init__.pyi,sha256=NCLMWT3G0Z5lJky8EV_N6XcW8smScti6y2p163nZ8rM,7965
3
+ ducktools/classbuilder/_version.py,sha256=GkzZtP41uShUoMdcKAWBkfQEBwmWhBFNXQ0WU1xWj5c,52
4
+ ducktools/classbuilder/annotations.py,sha256=x-3936HcJzv04rga8k0fFTbu1656yDglVN6kCan4hiw,8203
5
+ ducktools/classbuilder/annotations.pyi,sha256=vW3YQIiKYYQJll9_B4oTkBwIh4nOy1AnU9Ny8_a0dRY,686
6
+ ducktools/classbuilder/prefab.py,sha256=6YOVVYYeuMF5gv9DnK-sCvNKxBaBoqDXNLUQ22-xINk,24097
7
+ ducktools/classbuilder/prefab.pyi,sha256=x2ioTpkhNjQXFeKABFOzE0xNeZ8f_W5vusmuAzE19mc,4491
8
+ ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
9
+ ducktools_classbuilder-0.7.1.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
10
+ ducktools_classbuilder-0.7.1.dist-info/METADATA,sha256=Jo1LiPtmH3i5Z5N4QpjZkqtXAf59jYGbs1uVzMSCmLQ,10968
11
+ ducktools_classbuilder-0.7.1.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
12
+ ducktools_classbuilder-0.7.1.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
13
+ ducktools_classbuilder-0.7.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.3.0)
2
+ Generator: setuptools (72.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,12 +0,0 @@
1
- ducktools/classbuilder/__init__.py,sha256=cU3AdLjUWlYA_yUo5uMv2CyrzYNbnnZogsOVAmCn-Dw,32820
2
- ducktools/classbuilder/__init__.pyi,sha256=JaAfc6y4Jj920y4lm9peduNXeb9XUJwKN-QqFpqG5pw,7924
3
- ducktools/classbuilder/annotations.py,sha256=17g1WnWfyMWBHKQp2HD_hvV-n7CKXy_zv8NTODpOsuU,5437
4
- ducktools/classbuilder/annotations.pyi,sha256=cTZQ-pp2IkfdvwHiU3pIySUzzBKxfOtO-e5PkA8TNwo,520
5
- ducktools/classbuilder/prefab.py,sha256=C70mxApT6pOe18Clz9sCajWWdhHTJjMAyjx2zVZPJew,23696
6
- ducktools/classbuilder/prefab.pyi,sha256=iZsrju2sU_5_rOoNFrG4-vNTRrZr3dSxWNzmHd2BuYw,4387
7
- ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
8
- ducktools_classbuilder-0.6.3.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
9
- ducktools_classbuilder-0.6.3.dist-info/METADATA,sha256=Qxqxy6400PNSK2wb1pEHx_99rKVSym70110QesZtdnc,10911
10
- ducktools_classbuilder-0.6.3.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
11
- ducktools_classbuilder-0.6.3.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
12
- ducktools_classbuilder-0.6.3.dist-info/RECORD,,