Skip to content

Unexpected behavior accessing attributes provided by tp_getset #126752

@zhangyx1998

Description

@zhangyx1998

Bug report

Bug description:

The problem is initially presented in this post on python discussion forum (under topic "python help")

========== BEGIN Mirror of original post ==========

Background:

In my latest commit 3375c80, I have a builtin function expose that creates a DeferExprExposed object which provides access to otherwise hidden attributes on DeferExpr objects (link to code).

Observed Problem:

The attributes are provided through PyTypeObject.tp_getset hooks (link to code). However, I observed some really wired behavior with them:

  1. First attribute access fails, 2nd time OK (and so on):

    >>> x => 1 # Creates a DeferExpr object `x`
    >>> expose(x).callable # 1st try fails
    AttributeError: 'DeferExprExposed' object has no attribute 'callable'
    >>> expose(x).callable # 2nd try works
    <function <lambda> at 0x10395a560>
    

    Note that expose(x) returns a new DeferExprExposed object each time. It does not reuse the same exposed object. (i.e. two tries happens on two different objects).

  2. Accessing attribute on the same object fails on 1st try, but succeed otherwise:

    >>> x => 1 # Creates a DeferExpr object `x`
    >>> e = expose(x)
    >>> e.callable # 1st try fails
    AttributeError: 'DeferExprExposed' object has no attribute 'callable'
    >>> e.callable # 2nd try works
    <function <lambda> at 0x100df2a30>
=========== END Mirror of original post ===========

Expected behavior

Attribute access should alway success.

Reproducible Example

Commit 958f53e @ zhangyx1998/cpython can be used to reproduce this problem.

In commit 3e3b7d4, tp_getset was replaced by tp_setattr and tp_getattr, and the problem no longer exists. This indicates the problem is unlikely to be caused by internal logic of tp_getset hooks.

Possible Solution

In C API PyObject_GetAttr(), add a for-loop to traverse tp_getset and match listed attributes with given name. Traversal of tp_getset should be the last resort (i.e. only executed when tp_getattr and tp_getattro are both NULL).

This logic is not found anywhere in the current code.

P.S. I do not really understand why the second access to the same attribute "magically" works. According to the logic shown in linked code, it should always raise the same attribute error.

CPython versions tested on:

CPython main branch

Operating systems tested on:

macOS

Metadata

Metadata

Assignees

No one assigned

    Labels

    type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions