Skip to content

Propagate errors from typing.ClassVar attribute lookup (#1221) - #1223

Merged
sobolevn merged 3 commits into
msgspec:mainfrom
charan-rathore:fix-1221-classvar-lookup-error
Oct 11, 2026
Merged

sobolevn merged 3 commits into
msgspec:mainfrom
charan-rathore:fix-1221-classvar-lookup-error

Conversation

@charan-rathore

@charan-rathore charan-rathore commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1221

The #1097 fix looks up ClassVar on whatever typing names in the module
namespace, but if that attribute lookup itself raises, the error return is
missed: structmeta_is_classvar compares the NULL result against
typing.ClassVar, reports "not a ClassVar", and returns 0 with the
exception still pending. On CPython builds with assertions enabled this
trips assert(!PyErr_Occurred()) in type_new_init and terminates the
interpreter.

Return -1 when the lookup fails, matching the error handling already used
for the __origin__ lookup just below. The original AttributeError from
#1096 still propagates exactly as before.

Tests: the existing test_wrong_classvar regression is parametrized to also
cover the subscripted form typing.ClassVar[int], and a new test covers a
typing stand-in whose ClassVar attribute raises an arbitrary error, so
the propagation path itself is pinned rather than only the #1096 case.

Verified on a locally built CPython 3.13.7 --with-pydebug: the unpatched
code stops the interpreter in test_wrong_classvar (exit 134); with the
patch the full unit suite passes (6298 passed, 313 skipped). Focused tests
also pass on a regular 3.13 build, which is what CI runs.

Changelog entry included under Unreleased.

Tested on Python 3.13 only (debug and regular builds). CI covers 3.10 to 3.15; the change is a two-line error check in version-independent C.

@Siyet Siyet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you remove the claim that regular builds silently dropped the lookup error? On regular CPython 3.12.13, the original test already raises AttributeError before this change, and the property case also preserves the original RuntimeError. The fix prevents the assertion failure by returning immediately when the lookup fails.

@charan-rathore

Copy link
Copy Markdown
Contributor Author

Thanks, you're right. I dropped that sentence from the description. On a regular build the original error still surfaces; the fix is about the assertion failure on debug builds, where returning right away when the lookup fails avoids hitting assert(!PyErr_Occurred()).

Siyet commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Thx, could you also remove that claim from docs/changelog.md? It is still present in the Unreleased entry.

@charan-rathore

Copy link
Copy Markdown
Contributor Author

Done, removed it from the changelog entry too (ccc3a24).

Siyet
Siyet previously approved these changes Oct 1, 2026
Comment thread tests/unit/test_struct.py Outdated
a: typing.ClassVar
"""

source = source.replace("a: typing.ClassVar", f"a: {annotation}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we instead use f string to format source directly? It does not seem safe now.

Comment thread tests/unit/test_struct.py Resolved
Per review: interpolate the parameterized annotation directly into the
module template instead of running str.replace over it.

Signed-off-by: Charan Rathore <[email protected]>
@charan-rathore

Copy link
Copy Markdown
Contributor Author

Thanks for the review, both fair points.

The f-string: agreed, done in 1bdc3a1. The template now interpolates {annotation} directly instead of running replace over the rendered source. The ClassVar tests pass before and after (15 passed under tests/unit/test_struct.py -k classvar).

The with block: not strictly required, no. temp_module only removes the module from sys.modules in its finally, so the old __enter__()-only form leaked a temp_<uuid> entry per run, and the parametrize makes the test run twice. Since I was already touching the test I switched it to the proper context-manager form. Happy to split that back out if you would rather keep the diff minimal.

@sobolevn sobolevn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@sobolevn
sobolevn added this pull request to the merge queue Oct 11, 2026
Merged via the queue into msgspec:main with commit 97a49ee Oct 11, 2026
23 of 24 checks passed
charan-rathore added a commit to charan-rathore/charan-rathore that referenced this pull request Oct 11, 2026
@charan-rathore
charan-rathore deleted the fix-1221-classvar-lookup-error branch October 11, 2026 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0.22.0 regression: tests/unit/test_struct.py::TestClassVar::test_wrong_classvar is breaking assertions in CPython

3 participants