Skip to content

Let a shadowing class attribute win over the exception setattr/delattr arms - #1967

Merged
youknowone merged 2 commits into
mainfrom
perf-exc
Sep 26, 2026
Merged

youknowone merged 2 commits into
mainfrom
perf-exc

Conversation

@youknowone

@youknowone youknowone commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

characters_written read side (first commit)

#1959 landed a version of the unset-characters_written getter that returned
Err directly, which short-circuits the MRO lookup so a subclass class
attribute could not win:

class E(OSError): characters_written = 42
E().characters_written      # raised; should read 42

The follow-up that fixed it (Ok(PY_NULL), letting the caller continue, with
the raise staying in exception_getset_fget where the descriptor is) was
dropped by #1959's squash, so it is recovered here. The same commit carries the
three traceback() / tb_next messages from that review.


object_setattr and object_delattr walk the type MRO into w_descr before
the store, then run an if is_exception(obj) arm that reaches the interpreter
slot by name. The arm fired regardless of what the walk found, so a subclass
that shadows an exception slot name with a plain class attribute lost the
write entirely:

class E(OSError): errno = 99
e = E(1, 'm')
e.errno = 'SET'
e.errno, e.__dict__     # was (99, {}),                 now ('SET', {'errno': 'SET'})
del e.errno
e.errno, e.__dict__     # was (None, {'errno': None}),  now (99, {})

The arms stand in for a GetSetProperty.__set__ / __delete__, which only run
when the MRO resolved to that descriptor. descr__setattr__ continues to
setdictvalue and descr__delattr__ to deldictvalue when the walk found a
non-data descriptor, so both arms are now gated on w_descr.is_none(). The
read side already had this ordering — its exception arm lives in
object_getattr_miss, after the walk — which is why only the two write paths
were affected.

The unshadowed paths are unchanged, because every exception GetSetProperty
carries the one shared exception_getset_fset / exception_getset_fdel and
those call exception_attr_set / exception_attr_delete themselves:
OSError().errno = 5 still writes the slot through the descriptor,
BaseExceptionGroup(...).message = 'z' still raises readonly attribute,
del OSError().errno still resets the slot, and an undeclared name still
reaches setdictvalue with w_descr unset.

Measurement

32 exception slot attributes across 9 classes under a shadowing subclass, five
patterns each — read, set-then-read, check __dict__, set-del-read, del-unset.

cases pyre before pyre after
pypy3 == CPython 3.14 144 62 differ 0 differ
upstreams disagree (del-unset) 32 matched neither matches pypy3

Where both upstreams agree, pyre was alone — the arms were a pyre-only
deviation with no upstream counterpart, not a spec question.

On del of a name that was never set the two upstreams disagree, and pyre
previously matched neither: it reset the slot and reported the delete as having
succeeded. It now reaches the shared raiseattrerror(obj, name, w_descr)
terminal descr__delattr__ ends in, which reports a found-but-non-data
descriptor as 'E' object attribute 'errno' is read-only. 3.14 says
'E' object has no attribute 'errno' there. That remaining difference is
raiseattrerror's own message shape, shared with the setattr terminal, and is
left as it stands.

— commented by Claude

Summary by CodeRabbit

  • Bug Fixes
    • Exception attributes now resolve correctly when subclasses define their own characters_written value. Missing values produce a more specific error.
    • Traceback constructor errors now identify missing arguments and the maximum number of arguments allowed. Errors for invalid traceback frames and tb_next values provide clearer details.

Unset OSError.characters_written continues MRO lookup so
`class E(OSError): characters_written = 42` reads 42. The OSError
getset still raises AttributeError("characters_written").

traceback() constructor and tb_next setter TypeError text match
the 3.14 named-argument form.

Assisted-by: Claude
…r arms

`object_setattr` and `object_delattr` walk the type MRO into `w_descr`
before the store.  A subclass that shadows an exception slot name with a
plain class attribute (`class E(OSError): errno = 99`) contributes no
`__set__`/`__delete__`, so `descr__setattr__` continues to `setdictvalue`
and `descr__delattr__` to `deldictvalue`.  The `if is_exception(obj)` arms
below both fired by name regardless and reached the interpreter slot, so
the assignment never got to the instance dict and the removal never took
the dict entry back out:

    class E(OSError): errno = 99
    e = E(1, 'm'); e.errno = 'SET'
    e.errno, e.__dict__     # was (99, {}),   now ('SET', {'errno': 'SET'})
    del e.errno
    e.errno, e.__dict__     # was (None, {'errno': None}), now (99, {})

The arms stand in for a `GetSetProperty.__set__` / `__delete__`, which
only run when the MRO resolved to that descriptor, so gate both on the
walk having found nothing.  Every exception `GetSetProperty` carries the
one shared `exception_getset_fset` / `exception_getset_fdel`, which call
`exception_attr_set` / `exception_attr_delete` themselves, so the
unshadowed paths are unchanged: `OSError().errno = 5` still writes the
slot through the descriptor, `BaseExceptionGroup(...).message = 'z'` still
raises `readonly attribute`, `del OSError().errno` still resets the slot,
and an undeclared name still reaches `setdictvalue` with `w_descr` unset.

Measured over 32 exception slot attributes across 9 classes under a
shadowing subclass, five patterns each (read, set-then-read, check
`__dict__`, set-del-read, del-unset).  `pypy3` and CPython 3.14 agree on
the first four, 144 cases: pyre differed on 62 before this and none
after, so the arms were a pyre-only deviation with no upstream
counterpart rather than a spec question.

On the fifth, `del` of a name that was never set, the two upstreams
disagree and pyre matched neither: it reset the slot and reported the
delete as having succeeded.  It now reaches the shared
`raiseattrerror(obj, name, w_descr)` terminal that `descr__delattr__`
ends in, which reports a found-but-non-data descriptor as
`'E' object attribute 'errno' is read-only` -- PyPy's message, where 3.14
says `'E' object has no attribute 'errno'`.  That difference is
`raiseattrerror`'s own shape, shared with the setattr terminal, and is
left as it stands.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 3cd8c0ae-a541-4ffe-8a75-f3a9ba3129a2

📥 Commits

Reviewing files that changed from the base of the PR and between 45e4d75 and 1fb3764.

📒 Files selected for processing (3)
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/typedef.rs

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.


Walkthrough

The changes update exception attribute lookup and descriptor handling. They also revise argument and type error messages for traceback construction and tb_next assignment.

Changes

Exception attribute handling

Layer / File(s) Summary
Unset characters_written lookup
pyre/pyre-interpreter/src/baseobjspace.rs, pyre/pyre-interpreter/src/builtins.rs
When characters_written is unset, lookup can check subclass attributes before the getter raises an attribute-specific AttributeError.
Exception descriptor assignment and deletion
pyre/pyre-interpreter/src/baseobjspace.rs
Exception-specific assignment and deletion handling runs only when no descriptor was found.

Traceback error reporting

Layer / File(s) Summary
Traceback constructor and setter errors
pyre/pyre-interpreter/src/typedef.rs
Constructor arity errors identify missing arguments or the four-argument limit. The tb_frame error names traceback(), and invalid tb_next values are reported by type.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 1fb37

The intended exception attribute behavior is supported by the inspected lookup paths, and no actionable issue remains before merge.

Architecture Summary

Architecture risk: 🔵 Low · up to 1fb37

The change affects 1 system.

Changed systems: pyre

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — pyre (service) was modified; 3 changed files map to changed impact.

Before / after behavior

  • observed — Modified behavior in pyre/pyre-interpreter/src/baseobjspace.rs: When characters_written is unset, the lookup now returns PY_NULL instead of immediately raising AttributeError, allowing ordinary lookup to check subclass attributes first.
  • observed — Modified behavior in pyre/pyre-interpreter/src/baseobjspace.rs: Exception-specific attribute assignment now runs only when w_descr is absent; previously it ran for exceptions regardless. When a descriptor was found, assignment proceeds through the descriptor path.
  • observed — Modified behavior in pyre/pyre-interpreter/src/baseobjspace.rs: Exception-specific attribute deletion now runs only when w_descr is absent; previously it ran for exceptions regardless. When a descriptor was found, deletion proceeds through the descriptor path.
  • observed — Modified behavior in pyre/pyre-interpreter/src/builtins.rs: When the requested attribute is characters_written and its value is null, the getter now returns an AttributeError for characters_written; other unset attributes continue to use the existing generic absent-attribute path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: allowing a shadowing class attribute to take precedence over the exception-specific setattr and delattr handling.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks the traceback call,
Then finds the unset field in the hall.
Descriptors guide each set and clear,
And clearer errors now appear.
With tidy hops, the patch is done.

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 1fb3764).
Updated: 2026-09-25T23:44:52.345Z

Files in the reviewed diff
pyre/pyre-interpreter/src/baseobjspace.rs
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/typedef.rs

Codex did not produce a report (exit 1). Last log lines:

warning: Codex could not find bubblewrap on PATH. Install bubblewrap with your OS package manager. See the sandbox prerequisites: https://developers.openai.com/codex/concepts/sandboxing#prerequisites. Codex will use the bundled bubblewrap in the meantime.
2026-09-25T23:44:36.493057Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:36.603614Z ERROR rmcp::transport::worker: worker quit with fatal: Transport channel closed, when UnexpectedServerResponse("HTTP 401: {\n  \"error\": {\n    \"message\": \"Provided authentication token is expired. Please try signing in again.\",\n    \"type\": \"invalid_request_error\",\n    \"code\": \"token_expired\",\n    \"param\": null\n  },\n  \"status\": 401,\n  \"detail\": {\n    \"code\": \"token_expired\",\n    \"message\": \"Provided authentication token is expired. Please try signing in again.\"\n  }\n}")
2026-09-25T23:44:36.605380Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:36.605433Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:36.605479Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:36.731633Z ERROR rmcp::transport::worker: worker quit with fatal: Transport channel closed, when UnexpectedServerResponse("HTTP 401: {\n  \"error\": {\n    \"message\": \"Provided authentication token is expired. Please try signing in again.\",\n    \"type\": \"invalid_request_error\",\n    \"code\": \"token_expired\",\n    \"param\": null\n  },\n  \"status\": 401,\n  \"detail\": {\n    \"code\": \"token_expired\",\n    \"message\": \"Provided authentication token is expired. Please try signing in again.\"\n  }\n}")
2026-09-25T23:44:36.923522Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:37.050696Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:37.171982Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:37.277299Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:37.440305Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:37.440370Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:37.440417Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:37.838427Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:37.838509Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:37.838560Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
ERROR: Reconnecting... 2/5
2026-09-25T23:44:38.407005Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:38.407085Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:38.407131Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
ERROR: Reconnecting... 3/5
2026-09-25T23:44:39.361545Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:39.361619Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:39.361669Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
ERROR: Reconnecting... 4/5
2026-09-25T23:44:41.052411Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:41.052488Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:41.052537Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
ERROR: Reconnecting... 5/5
2026-09-25T23:44:44.565260Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:44.565338Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:44.565384Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:44.795292Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:44.795367Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:44.795416Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
warning: Falling back from WebSockets to HTTPS transport. workspace routing discovery unauthorized (401)
ERROR: Reconnecting... 1/5
2026-09-25T23:44:45.203529Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:45.203617Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:45.203668Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
ERROR: Reconnecting... 2/5
2026-09-25T23:44:45.817517Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:45.817595Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:45.817645Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
ERROR: Reconnecting... 3/5
2026-09-25T23:44:46.925140Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:46.925241Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:46.925293Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
ERROR: Reconnecting... 4/5
2026-09-25T23:44:48.688554Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:48.688628Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:48.688678Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
ERROR: Reconnecting... 5/5
2026-09-25T23:44:51.810504Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:51.810581Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-09-25T23:44:51.810631Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
ERROR: workspace routing discovery unauthorized (401)
2026-09-25T23:44:52.093720Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
ERROR: workspace routing discovery unauthorized (401)

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.

1 participant