Skip to content

Forward deletion of __module__ and __doc__ on a proxy to the wrapped object - #353

Merged
GrahamDumpleton merged 1 commit into
developfrom
bugfix/proxy-doc-module-delete
Sep 20, 2026
Merged

GrahamDumpleton merged 1 commit into
developfrom
bugfix/proxy-doc-module-delete

Conversation

@GrahamDumpleton

Copy link
Copy Markdown
Owner

Supersedes #351. Fixes the interpreter crash reported there.

Problem

Deleting __module__ or __doc__ on a proxy, e.g. del proxy.__doc__,
crashed the interpreter when the C extension was in use. CPython invokes
the attribute setter with a NULL value for a deletion; the setter forwarded
that correctly to the wrapped object, then passed the same NULL to
PyDict_SetItemString() when updating the copy held in the proxy's own
dict. Reachable from ordinary Python code, and present in every release
back to at least 1.x.

The pure Python implementation did not crash but raised
AttributeError: property '__doc__' of 'ObjectProxy' object has no deleter,
so the deletion was never forwarded at all. The two implementations
disagreed, and neither matched deleting the attribute on the wrapped object
directly.

Changes

  • C extension: set_module / set_doc distinguish a deletion from an
    assignment. After forwarding a deletion, a new helper refreshes the
    cached copy from the wrapped object, storing the current value if the
    attribute still exists (a function's __doc__ becomes None) or
    dropping the entry if it is gone. The proxy's __self_dict__ after a
    deletion is therefore identical to that of a proxy newly created over
    the wrapped object.
  • Pure Python: deleters on the __module__ and __doc__ properties.
    __delattr__ already routes names defined on the type to
    object.__delattr__, which now invokes them, so no special-casing.
  • Tests: deletion on ObjectProxy, FunctionWrapper and a user subclass;
    delete after set (the original crash path); class targets, where the
    outcome via the proxy is compared with a direct delattr on an
    equivalent class so it holds across CPython versions and PyPy;
    __self_dict__ parity with a fresh proxy; deletion via a non-interned
    attribute name, which reaches the same setters through setattro.
    Targets are created per test so nothing mutates the shared module.
  • Changelog entry under 2.4.2.

Relationship to #351

The crash, its cause and the pure Python divergence were identified by
Ding Qiuran in #351, which also proposed a fix. This PR takes a different
approach to the cache (refresh from the wrapped object rather than delete
the entry), adds broader tests, and leaves out the unrelated
enabled != Py_None guard that #351 also picked up. That guard addresses
a separate crash in calling an uninitialised FunctionWrapper, which
turned out to have three affected code paths rather than two, and will be
handled in its own PR.

Verification

just test passes across the full matrix (3.9 to 3.15, three variants
each). Cross-implementation parity for deletion was checked against
function, class, instance, int and module targets with identical
results.

…object.

Deleting __module__ or __doc__ on a proxy crashed the interpreter with the
C extension, as the attribute setter was invoked with a NULL value for the
deletion and passed that on to PyDict_SetItemString() when updating the
copy held in the proxy's own dict. The pure Python implementation raised
AttributeError instead, as the properties for these attributes had no
deleter, so the deletion was never forwarded to the wrapped object.

Both implementations now forward the deletion so the outcome matches
deleting the attribute on the wrapped object directly. The C extension
then refreshes its cached copy from the wrapped object, leaving the proxy
in the same state as one newly created over it.

The crash and its cause were identified by Ding Qiuran in #351, which
also proposed a fix. This supersedes that pull request with a wider set
of tests, and separates out the unrelated change it had also picked up.
@GrahamDumpleton
GrahamDumpleton merged commit 4070120 into develop Sep 20, 2026
49 checks passed
GrahamDumpleton added a commit that referenced this pull request Sep 20, 2026
Brings in the fix for deletion of __module__ and __doc__ on a proxy from
#353. Both changes added their release note at the same position for
version 2.4.2 in docs/changes.rst. Both are kept, with that for #353 first
as it was merged first.
@GrahamDumpleton
GrahamDumpleton deleted the bugfix/proxy-doc-module-delete branch September 20, 2026 09: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.

1 participant