Skip to content

Commit e772725

Browse files
committed
Cache generated un/structure functions using linecache
1 parent b079967 commit e772725

File tree

3 files changed

+156
-15
lines changed

3 files changed

+156
-15
lines changed

Diff for: HISTORY.rst

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ History
66
------------------
77
* Fix ``GenConverter`` mapping structuring for unannotated dicts on Python 3.8.
88
(`#151 <https://github.com/Tinche/cattrs/issues/151>`_)
9+
* The source code for generated un/structuring functions is stored in the `linecache` cache, which enables more informative stack traces when un/structuring errors happen using the `GenConverter`. This behavior can optionally be disabled to save memory.
910

1011
1.7.1 (2021-05-28)
1112
------------------

Diff for: src/cattr/gen.py

+83-15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import linecache
12
import re
3+
import uuid
24
from dataclasses import is_dataclass
35
from typing import Any, Optional, Type, TypeVar
46

@@ -21,7 +23,13 @@ def override(omit_if_default=None, rename=None):
2123
_neutral = AttributeOverride()
2224

2325

24-
def make_dict_unstructure_fn(cl, converter, omit_if_default=False, **kwargs):
26+
def make_dict_unstructure_fn(
27+
cl,
28+
converter,
29+
omit_if_default: bool = False,
30+
_cattrs_use_linecache: bool = True,
31+
**kwargs,
32+
):
2533
"""Generate a specialized dict unstructuring function for an attrs class."""
2634
cl_name = cl.__name__
2735
fn_name = "unstructure_" + cl_name
@@ -31,7 +39,7 @@ def make_dict_unstructure_fn(cl, converter, omit_if_default=False, **kwargs):
3139

3240
attrs = adapted_fields(cl) # type: ignore
3341

34-
lines.append(f"def {fn_name}(i):")
42+
lines.append(f"def {fn_name}(instance):")
3543
lines.append(" res = {")
3644
for a in attrs:
3745
attr_name = a.name
@@ -50,11 +58,11 @@ def make_dict_unstructure_fn(cl, converter, omit_if_default=False, **kwargs):
5058
is_identity = handler == converter._unstructure_identity
5159

5260
if not is_identity:
53-
unstruct_handler_name = f"__cattr_unstruct_handler_{attr_name}"
61+
unstruct_handler_name = f"unstructure_{attr_name}"
5462
globs[unstruct_handler_name] = handler
55-
invoke = f"{unstruct_handler_name}(i.{attr_name})"
63+
invoke = f"{unstruct_handler_name}(instance.{attr_name})"
5664
else:
57-
invoke = f"i.{attr_name}"
65+
invoke = f"instance.{attr_name}"
5866

5967
if d is not attr.NOTHING and (
6068
(omit_if_default and override.omit_if_default is not False)
@@ -66,14 +74,18 @@ def make_dict_unstructure_fn(cl, converter, omit_if_default=False, **kwargs):
6674
globs[def_name] = d.factory
6775
if d.takes_self:
6876
post_lines.append(
69-
f" if i.{attr_name} != {def_name}(i):"
77+
f" if instance.{attr_name} != {def_name}(instance):"
7078
)
7179
else:
72-
post_lines.append(f" if i.{attr_name} != {def_name}():")
80+
post_lines.append(
81+
f" if instance.{attr_name} != {def_name}():"
82+
)
7383
post_lines.append(f" res['{kn}'] = {invoke}")
7484
else:
7585
globs[def_name] = d
76-
post_lines.append(f" if i.{attr_name} != {def_name}:")
86+
post_lines.append(
87+
f" if instance.{attr_name} != {def_name}:"
88+
)
7789
post_lines.append(f" res['{kn}'] = {invoke}")
7890

7991
else:
@@ -82,10 +94,17 @@ def make_dict_unstructure_fn(cl, converter, omit_if_default=False, **kwargs):
8294
lines.append(" }")
8395

8496
total_lines = lines + post_lines + [" return res"]
97+
script = "\n".join(total_lines)
8598

86-
eval(compile("\n".join(total_lines), "", "exec"), globs)
99+
fname = _generate_unique_filename(
100+
cl, "unstructure", reserve=_cattrs_use_linecache
101+
)
102+
103+
eval(compile(script, fname, "exec"), globs)
87104

88105
fn = globs[fn_name]
106+
if _cattrs_use_linecache:
107+
linecache.cache[fname] = len(script), None, total_lines, fname
89108

90109
return fn
91110

@@ -110,7 +129,11 @@ def generate_mapping(cl: Type, old_mapping):
110129

111130

112131
def make_dict_structure_fn(
113-
cl: Type, converter, _cattrs_forbid_extra_keys: bool = False, **kwargs
132+
cl: Type,
133+
converter,
134+
_cattrs_forbid_extra_keys: bool = False,
135+
_cattrs_use_linecache: bool = True,
136+
**kwargs,
114137
):
115138
"""Generate a specialized dict structuring function for an attrs class."""
116139

@@ -167,20 +190,20 @@ def make_dict_structure_fn(
167190
else:
168191
handler = converter.structure
169192

170-
struct_handler_name = f"__cattr_struct_handler_{an}"
193+
struct_handler_name = f"structure_{an}"
171194
globs[struct_handler_name] = handler
172195

173196
ian = an if (is_dc or an[0] != "_") else an[1:]
174197
kn = an if override.rename is None else override.rename
175-
globs[f"__c_t_{an}"] = type
198+
globs[f"type_{an}"] = type
176199
if a.default is NOTHING:
177200
lines.append(
178-
f" '{ian}': {struct_handler_name}(o['{kn}'], __c_t_{an}),"
201+
f" '{ian}': {struct_handler_name}(o['{kn}'], type_{an}),"
179202
)
180203
else:
181204
post_lines.append(f" if '{kn}' in o:")
182205
post_lines.append(
183-
f" res['{ian}'] = {struct_handler_name}(o['{kn}'], __c_t_{an})"
206+
f" res['{ian}'] = {struct_handler_name}(o['{kn}'], type_{an})"
184207
)
185208
lines.append(" }")
186209
if _cattrs_forbid_extra_keys:
@@ -196,7 +219,20 @@ def make_dict_structure_fn(
196219

197220
total_lines = lines + post_lines + [" return __cl(**res)"]
198221

199-
eval(compile("\n".join(total_lines), "", "exec"), globs)
222+
fname = _generate_unique_filename(
223+
cl, "structure", reserve=_cattrs_use_linecache
224+
)
225+
script = "\n".join(total_lines)
226+
eval(
227+
compile(
228+
script,
229+
fname,
230+
"exec",
231+
),
232+
globs,
233+
)
234+
if _cattrs_use_linecache:
235+
linecache.cache[fname] = len(script), None, total_lines, fname
200236

201237
return globs[fn_name]
202238

@@ -396,3 +432,35 @@ def make_mapping_structure_fn(
396432
fn = globs[fn_name]
397433

398434
return fn
435+
436+
437+
def _generate_unique_filename(cls, func_name, reserve=True):
438+
"""
439+
Create a "filename" suitable for a function being generated.
440+
"""
441+
unique_id = uuid.uuid4()
442+
extra = ""
443+
count = 1
444+
445+
while True:
446+
unique_filename = "<cattrs generated {0} {1}.{2}{3}>".format(
447+
func_name,
448+
cls.__module__,
449+
getattr(cls, "__qualname__", cls.__name__),
450+
extra,
451+
)
452+
if not reserve:
453+
return unique_filename
454+
# To handle concurrency we essentially "reserve" our spot in
455+
# the linecache with a dummy line. The caller can then
456+
# set this value correctly.
457+
cache_line = (1, None, (str(unique_id),), unique_filename)
458+
if (
459+
linecache.cache.setdefault(unique_filename, cache_line)
460+
== cache_line
461+
):
462+
return unique_filename
463+
464+
# Looks like this spot is taken. Try again.
465+
count += 1
466+
extra = "-{0}".format(count)

Diff for: tests/test_gen.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Tests for functionality from the gen module."""
2+
import linecache
3+
from traceback import format_exc
4+
5+
from attr import define
6+
7+
from cattr import GenConverter
8+
from cattr.gen import make_dict_structure_fn, make_dict_unstructure_fn
9+
10+
11+
def test_structure_linecache():
12+
"""Linecaching for structuring should work."""
13+
14+
@define
15+
class A:
16+
a: int
17+
18+
c = GenConverter()
19+
try:
20+
c.structure({"a": "test"}, A)
21+
except ValueError:
22+
res = format_exc()
23+
assert "'a'" in res
24+
25+
26+
def test_unstructure_linecache():
27+
"""Linecaching for unstructuring should work."""
28+
29+
@define
30+
class Inner:
31+
a: int
32+
33+
@define
34+
class Outer:
35+
inner: Inner
36+
37+
c = GenConverter()
38+
try:
39+
c.unstructure(Outer({}))
40+
except AttributeError:
41+
res = format_exc()
42+
assert "'a'" in res
43+
44+
45+
def test_no_linecache():
46+
"""Linecaching should be disableable."""
47+
48+
@define
49+
class A:
50+
a: int
51+
52+
c = GenConverter()
53+
before = len(linecache.cache)
54+
c.structure(c.unstructure(A(1)), A)
55+
after = len(linecache.cache)
56+
57+
assert after == before + 2
58+
59+
@define
60+
class B:
61+
a: int
62+
63+
before = len(linecache.cache)
64+
c.register_structure_hook(
65+
B, make_dict_structure_fn(B, c, _cattrs_use_linecache=False)
66+
)
67+
c.register_unstructure_hook(
68+
B, make_dict_unstructure_fn(B, c, _cattrs_use_linecache=False)
69+
)
70+
c.structure(c.unstructure(B(1)), B)
71+
72+
assert len(linecache.cache) == before

0 commit comments

Comments
 (0)