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.
- ducktools/classbuilder/__init__.py +3 -11
- ducktools/classbuilder/__init__.pyi +1 -0
- ducktools/classbuilder/_version.py +2 -0
- ducktools/classbuilder/annotations.py +106 -23
- ducktools/classbuilder/annotations.pyi +6 -0
- ducktools/classbuilder/prefab.py +15 -1
- ducktools/classbuilder/prefab.pyi +2 -0
- {ducktools_classbuilder-0.6.3.dist-info → ducktools_classbuilder-0.7.1.dist-info}/METADATA +3 -2
- ducktools_classbuilder-0.7.1.dist-info/RECORD +13 -0
- {ducktools_classbuilder-0.6.3.dist-info → ducktools_classbuilder-0.7.1.dist-info}/WHEEL +1 -1
- ducktools_classbuilder-0.6.3.dist-info/RECORD +0 -12
- {ducktools_classbuilder-0.6.3.dist-info → ducktools_classbuilder-0.7.1.dist-info}/LICENSE.md +0 -0
- {ducktools_classbuilder-0.6.3.dist-info → ducktools_classbuilder-0.7.1.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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 = "
|
|
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
|
|
@@ -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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
except KeyError:
|
|
123
|
-
obj_module = None
|
|
189
|
+
if annotate is not None:
|
|
190
|
+
raw_annotations = call_annotate_func(annotate)
|
|
124
191
|
else:
|
|
125
|
-
|
|
192
|
+
raw_annotations = ns.get("__annotations__", {})
|
|
126
193
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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,
|
ducktools/classbuilder/prefab.py
CHANGED
|
@@ -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.
|
|
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,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,,
|
{ducktools_classbuilder-0.6.3.dist-info → ducktools_classbuilder-0.7.1.dist-info}/LICENSE.md
RENAMED
|
File without changes
|
{ducktools_classbuilder-0.6.3.dist-info → ducktools_classbuilder-0.7.1.dist-info}/top_level.txt
RENAMED
|
File without changes
|