|
28 | 28 | from collections.abc import Callable, Sequence |
29 | 29 | from typing import Any, Literal, ParamSpec, TypeVar |
30 | 30 |
|
| 31 | +import attrs |
31 | 32 | import cattrs |
32 | 33 | from attrs import Factory, define, field |
| 34 | +from cattrs.strategies import configure_tagged_union |
33 | 35 |
|
34 | 36 | from .analyzer_utils import dotted_path |
35 | 37 |
|
@@ -339,6 +341,14 @@ class TypeAlias(TopLevel): |
339 | 341 |
|
340 | 342 | TopLevelUnion = Class | Interface | Function | Attribute | TypeAlias |
341 | 343 |
|
| 344 | +# Resolve string forward references (e.g. ``list["Attribute"]``) in the |
| 345 | +# annotations of our attrs classes. |
| 346 | +for _cls in list(globals().values()): |
| 347 | + if isinstance(_cls, type) and attrs.has(_cls): |
| 348 | + attrs.resolve_types(_cls) |
| 349 | + |
| 350 | +del _cls |
| 351 | + |
342 | 352 | # Now make a serializer/deserializer. |
343 | 353 | # TODO: Add tests to make sure that serialization and deserialization are a |
344 | 354 | # round trip. |
@@ -379,12 +389,20 @@ def structure_description(x: Any, _: Any) -> Description | bool: |
379 | 389 | return converter.structure(x, list[DescriptionItem]) |
380 | 390 |
|
381 | 391 |
|
382 | | -def get_type_literal(t: type[DescriptionText]) -> str: |
| 392 | +def get_field_literal(t: type, field_name: str) -> str: |
| 393 | + """Take the "blah" from a ``Literal`` type annotation like |
| 394 | +
|
| 395 | + field_name: Literal["blah"] |
| 396 | + """ |
| 397 | + return t.__annotations__[field_name].__args__[0] # type:ignore[no-any-return] |
| 398 | + |
| 399 | + |
| 400 | +def get_type_literal(t: type) -> str: |
383 | 401 | """Take the "blah" from the type annotation in |
384 | 402 |
|
385 | 403 | type: Literal["blah"] |
386 | 404 | """ |
387 | | - return t.__annotations__["type"].__args__[0] # type:ignore[no-any-return] |
| 405 | + return get_field_literal(t, "type") |
388 | 406 |
|
389 | 407 |
|
390 | 408 | description_type_map = { |
@@ -417,3 +435,42 @@ def structure_str_or_nodefault(x: Any, _: Any) -> str | _NoDefault: |
417 | 435 | if isinstance(x, str): |
418 | 436 | return x |
419 | 437 | return NO_DEFAULT |
| 438 | + |
| 439 | + |
| 440 | +# Before cattrs version 25.2, it structured ``Sequence`` fields into lists, |
| 441 | +# afterwards it structures into tuples. Explicitly destructure to list so the |
| 442 | +# result is consistent across cattrs versions. |
| 443 | +def structure_description_sequence(x: Any, _: Any) -> list[Description]: |
| 444 | + return [converter.structure(e, Description) for e in x] # type:ignore[arg-type] |
| 445 | + |
| 446 | + |
| 447 | +converter.register_structure_hook_func( |
| 448 | + lambda t: t == Sequence[Description], |
| 449 | + structure_description_sequence, |
| 450 | +) |
| 451 | + |
| 452 | + |
| 453 | +# Configure tagged-union strategies for the unions we serialize. Each member |
| 454 | +# class carries a ``Literal`` discriminator field whose value we use as the tag: |
| 455 | +# ``type`` for TypeXRef and ``kind`` for the TopLevel unions. These are |
| 456 | +# configured last so that the custom structure hooks for the member fields (e.g. |
| 457 | +# Type, Description) are already registered, since ``configure_tagged_union`` |
| 458 | +# eagerly resolves the hooks for each member class. |
| 459 | +configure_tagged_union( |
| 460 | + TypeXRef, |
| 461 | + converter, |
| 462 | + tag_name="type", |
| 463 | + tag_generator=lambda cl: get_field_literal(cl, "type"), |
| 464 | +) |
| 465 | +configure_tagged_union( |
| 466 | + Function | Attribute, |
| 467 | + converter, |
| 468 | + tag_name="kind", |
| 469 | + tag_generator=lambda cl: get_field_literal(cl, "kind"), |
| 470 | +) |
| 471 | +configure_tagged_union( |
| 472 | + TopLevelUnion, |
| 473 | + converter, |
| 474 | + tag_name="kind", |
| 475 | + tag_generator=lambda cl: get_field_literal(cl, "kind"), |
| 476 | +) |
0 commit comments