|
8 | 8 | from datetime import date, datetime |
9 | 9 | from enum import Enum |
10 | 10 | from functools import partial |
| 11 | +from threading import local |
11 | 12 | from typing import Any, TypeVar, Union, get_type_hints |
12 | 13 |
|
13 | 14 | from attrs import has as attrs_has |
|
33 | 34 | from ..strategies import configure_union_passthrough |
34 | 35 | from . import literals_with_enums_unstructure_factory, wrap |
35 | 36 |
|
36 | | -T = TypeVar("T") |
37 | | - |
38 | 37 | __all__ = ["MsgspecJsonConverter", "configure_converter", "make_converter"] |
39 | 38 |
|
| 39 | +T = TypeVar("T") |
| 40 | +_already_probing = local() |
| 41 | +"""Used to detect and handle recursive data structures.""" |
| 42 | + |
40 | 43 |
|
41 | 44 | class MsgspecJsonConverter(Converter): |
42 | 45 | """A converter specialized for the _msgspec_ library.""" |
@@ -141,7 +144,7 @@ def seq_unstructure_factory(type, converter: Converter) -> UnstructureHook: |
141 | 144 | return converter.gen_unstructure_iterable(type) |
142 | 145 |
|
143 | 146 |
|
144 | | -def mapping_unstructure_factory(type, converter: BaseConverter) -> UnstructureHook: |
| 147 | +def mapping_unstructure_factory(type, converter: Converter) -> UnstructureHook: |
145 | 148 | """The msgspec unstructure hook factory for mappings.""" |
146 | 149 | if is_bare(type): |
147 | 150 | key_arg = Any |
@@ -176,20 +179,36 @@ def msgspec_attrs_unstructure_factory( |
176 | 179 | private attributes, making us do the work. |
177 | 180 | """ |
178 | 181 | origin = get_origin(type) |
179 | | - attribs = fields(origin or type) |
| 182 | + base = origin or type |
| 183 | + attribs = fields(base) |
180 | 184 | if attrs_has(type) and any(isinstance(a.type, str) for a in attribs): |
181 | 185 | resolve_types(type) |
182 | | - attribs = fields(origin or type) |
| 186 | + attribs = fields(base) |
183 | 187 |
|
184 | | - if msgspec_skips_private and any( |
185 | | - attr.name.startswith("_") |
186 | | - or ( |
| 188 | + if msgspec_skips_private and any(attr.name.startswith("_") for attr in attribs): |
| 189 | + # Private attributes we have to do ourselves. |
| 190 | + return converter.gen_unstructure_attrs_fromdict(type) |
| 191 | + |
| 192 | + try: |
| 193 | + working_set = _already_probing.working_set |
| 194 | + if base in working_set: |
| 195 | + return to_builtins |
| 196 | + except AttributeError: |
| 197 | + working_set = set() |
| 198 | + _already_probing.working_set = working_set |
| 199 | + |
| 200 | + try: |
| 201 | + working_set.add(base) |
| 202 | + if any( |
187 | 203 | converter.get_unstructure_hook(attr.type, cache_result=False) |
188 | 204 | not in (identity, to_builtins) |
189 | | - ) |
190 | | - for attr in attribs |
191 | | - ): |
192 | | - return converter.gen_unstructure_attrs_fromdict(type) |
| 205 | + for attr in attribs |
| 206 | + ): |
| 207 | + return converter.gen_unstructure_attrs_fromdict(type) |
| 208 | + finally: |
| 209 | + working_set.remove(base) |
| 210 | + if not working_set: |
| 211 | + del _already_probing.working_set |
193 | 212 |
|
194 | 213 | return to_builtins |
195 | 214 |
|
|
0 commit comments