Skip to content

Commit 5b869e5

Browse files
committed
Remove cap to cattrs < 25.1
1 parent 5b3388f commit 5b869e5

3 files changed

Lines changed: 64 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## Changelog
22

3+
### Unreleased
4+
5+
- Fixed compatibility with newer versions of cattrs (>= 25).
6+
37
### 5.0.3: (March 30th, 2026)
48

59
- Test Python 3.14 in CI (#302)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ authors = [
1313
requires-python = ">=3.10"
1414
dependencies = [
1515
"attrs",
16-
"cattrs<25.1",
16+
"cattrs",
1717
"Jinja2>2.0",
1818
"markupsafe",
1919
"parsimonious>=0.10.0,<0.11.0",

sphinx_js/ir.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
from collections.abc import Callable, Sequence
2929
from typing import Any, Literal, ParamSpec, TypeVar
3030

31+
import attrs
3132
import cattrs
3233
from attrs import Factory, define, field
34+
from cattrs.strategies import configure_tagged_union
3335

3436
from .analyzer_utils import dotted_path
3537

@@ -339,6 +341,14 @@ class TypeAlias(TopLevel):
339341

340342
TopLevelUnion = Class | Interface | Function | Attribute | TypeAlias
341343

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+
342352
# Now make a serializer/deserializer.
343353
# TODO: Add tests to make sure that serialization and deserialization are a
344354
# round trip.
@@ -379,12 +389,20 @@ def structure_description(x: Any, _: Any) -> Description | bool:
379389
return converter.structure(x, list[DescriptionItem])
380390

381391

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:
383401
"""Take the "blah" from the type annotation in
384402
385403
type: Literal["blah"]
386404
"""
387-
return t.__annotations__["type"].__args__[0] # type:ignore[no-any-return]
405+
return get_field_literal(t, "type")
388406

389407

390408
description_type_map = {
@@ -417,3 +435,42 @@ def structure_str_or_nodefault(x: Any, _: Any) -> str | _NoDefault:
417435
if isinstance(x, str):
418436
return x
419437
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

Comments
 (0)