Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

str(weakref) throws error KeyError: '__name__' if the original obj override __getattr__ #99184

Closed
yanboliang opened this issue Nov 7, 2022 · 6 comments
Labels
type-bug An unexpected behavior, bug, or error

Comments

@yanboliang
Copy link

yanboliang commented Nov 7, 2022

Bug report

A clear and concise description of what the bug is.
Include a minimal, reproducible example (https://stackoverflow.com/help/minimal-reproducible-example), if possible.

Repo:

class MyConfig(dict):
    def __getattr__(self, x):
        return self[x]

obj = MyConfig(offset=5)
obj_weakref = weakref.ref(obj)
str(obj_weakref)  # raise error: KeyError: '__name__'

Error:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __getattr__
KeyError: '__name__'

Your environment

  • CPython versions tested on: python 3.8 & 3.9
  • Operating system and architecture: Ubuntu Linux and MacOS
@sobolevn
Copy link
Member

sobolevn commented Nov 7, 2022

This does not look like a bug to me.
What do you expect to happen?

Right now weakrefobject.c has these lines:

static PyObject *
weakref_repr(PyWeakReference *self)
{
    PyObject *name, *repr;
    PyObject* obj = PyWeakref_GET_OBJECT(self);

    if (obj == Py_None) {
        return PyUnicode_FromFormat("<weakref at %p; dead>", self);
    }

    Py_INCREF(obj);
    if (_PyObject_LookupAttr(obj, &_Py_ID(__name__), &name) < 0) {
        Py_DECREF(obj);
        return NULL;
    }
    // ...

It tries to get __name__ attribute from the passed object. Since __getattr__ tries to get __name__ from self - it fails with a KeyError. It works exactly as it is written.

To fix this you can do something like:

class MyConfig(dict):
    def __getattr__(self, x):
        if x in {'__name__', '__class__', 'whatever_else'}:
             return getattr(type(self), x)
        return self[x]

@sobolevn sobolevn added the pending The issue will be closed if no feedback is provided label Nov 7, 2022
@yanboliang
Copy link
Author

yanboliang commented Nov 7, 2022

@sobolevn Thanks for your quick response! I do agree this works well in functionality's perspective, but I'd like to say if this is a good user experience. If users are just trying to override __getattr__ to implement their own functionality(like what I'm doing in the example), it means they should handle a lot of other python internal stuff at well. I think this would sort of increase the complexity, and make users can't focus on their own functionality.

@sobolevn
Copy link
Member

sobolevn commented Nov 7, 2022

So, probably you are asking from some kind of fallback for cases like this. Please, correct me if I am wrong :)

I think that repr() of weakref.ref would have a default value if _PyObject_LookupAttr(obj, &_Py_ID(__name__), &name) fails.

Example: <weakref at 0x1085b5800; to __some_default__ at 0x7ffd1071c080 (...)>

However, I don't think this is a good user experience either.
It is better to see an explicit exception early on than very strange and mostly unreadable repr somewhere in logs.

Considering your class, it looks problematic to me. It does not respect basic python's API, like: __dict__, __name__, __qualname__, __class__, __module__, etc.

They are quite basic, many other parts of CPython (and 3rd party projects) do use it.
And you will have a lot of other failure when these fundamentals cannot be accessed 🤔

Do you agree with me?

@yanboliang
Copy link
Author

yanboliang commented Nov 7, 2022

I can understand your point, if other parts of CPython do the same way, it's fine. But if you take a look at this example:

>>> class MyConfig(dict):
...         def __getattr__(self, x):
...             return self[x]
... 
>>> obj = MyConfig(offset=5)
>>> str(obj)
"{'offset': 5}"

If users don't user weakref, it works well even they don't handle __dict__, __name__, __qualname__, __class__, __module__, etc. Do you think the inconsistency will confuse users?

@MojoVampire
Copy link
Contributor

MojoVampire commented Nov 8, 2022

If the goal is to look up __name__ on the class of the instance, shouldn't the weakref code be using _PyObject_LookupSpecial (or _PyObject_LookupSpecialId, though I guess those are the same thing now?), not _PyObject_LookupAttr? The only advantage to the latter is if we wanted to allow instances to change their names by overwriting __name__. Using the LookupSpecial API would bypass the instance and avoid this issue (as well as being faster and likely more correct).

@sobolevn
Copy link
Member

sobolevn commented Nov 8, 2022

@MojoVampire yes, this is a good idea. Thank you! I didn't think about it :)

I've submitted #99244 with your proposal.

@sobolevn sobolevn removed the pending The issue will be closed if no feedback is provided label Nov 9, 2022
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Apr 24, 2023
…f.ref` (pythonGH-99244)

(cherry picked from commit 58b6be3)

Co-authored-by: Nikita Sobolev <mail@sobolevn.me>
carljm pushed a commit that referenced this issue Apr 24, 2023
…ef.ref` (GH-99244) (#103789)

gh-99184: Bypass instance attribute access in `repr` of `weakref.ref` (GH-99244)
(cherry picked from commit 58b6be3)

Co-authored-by: Nikita Sobolev <mail@sobolevn.me>
@carljm carljm closed this as completed Apr 24, 2023
carljm added a commit to carljm/cpython that referenced this issue Apr 24, 2023
* main: (53 commits)
  pythongh-102498 Clean up unused variables and imports in the email module  (python#102482)
  pythongh-99184: Bypass instance attribute access in `repr` of `weakref.ref` (python#99244)
  pythongh-99032: datetime docs: Encoding is no longer relevant (python#93365)
  pythongh-94300: Update datetime.strptime documentation (python#95318)
  pythongh-103776: Remove explicit uses of $(SHELL) from Makefile (pythonGH-103778)
  pythongh-87092: fix a few cases of incorrect error handling in compiler (python#103456)
  pythonGH-103727: Avoid advancing tokenizer too far in f-string mode (pythonGH-103775)
  Revert "Add tests for empty range equality (python#103751)" (python#103770)
  pythongh-94518: Port 23-argument `_posixsubprocess.fork_exec` to Argument Clinic (python#94519)
  pythonGH-65022: Fix description of copyreg.pickle function (python#102656)
  pythongh-103323: Get the "Current" Thread State from a Thread-Local Variable (pythongh-103324)
  pythongh-91687: modernize dataclass example typing (python#103773)
  pythongh-103746: Test `types.UnionType` and `Literal` types together (python#103747)
  pythongh-103765: Fix 'Warning: py:class reference target not found: ModuleSpec' (pythonGH-103769)
  pythongh-87452: Improve the Popen.returncode docs
  Removed unnecessary escaping of asterisks (python#103714)
  pythonGH-102973: Slim down Fedora packages in the dev container (python#103283)
  pythongh-103091: Add PyUnstable_Type_AssignVersionTag (python#103095)
  Add tests for empty range equality (python#103751)
  pythongh-103712: Increase the length of the type name in AttributeError messages (python#103713)
  ...
carljm added a commit to carljm/cpython that referenced this issue Apr 24, 2023
* superopt: (82 commits)
  pythongh-101517: fix line number propagation in code generated for except* (python#103550)
  pythongh-103780: Use patch instead of mock in asyncio unix events test (python#103782)
  pythongh-102498 Clean up unused variables and imports in the email module  (python#102482)
  pythongh-99184: Bypass instance attribute access in `repr` of `weakref.ref` (python#99244)
  pythongh-99032: datetime docs: Encoding is no longer relevant (python#93365)
  pythongh-94300: Update datetime.strptime documentation (python#95318)
  pythongh-103776: Remove explicit uses of $(SHELL) from Makefile (pythonGH-103778)
  pythongh-87092: fix a few cases of incorrect error handling in compiler (python#103456)
  pythonGH-103727: Avoid advancing tokenizer too far in f-string mode (pythonGH-103775)
  Revert "Add tests for empty range equality (python#103751)" (python#103770)
  pythongh-94518: Port 23-argument `_posixsubprocess.fork_exec` to Argument Clinic (python#94519)
  pythonGH-65022: Fix description of copyreg.pickle function (python#102656)
  pythongh-103323: Get the "Current" Thread State from a Thread-Local Variable (pythongh-103324)
  pythongh-91687: modernize dataclass example typing (python#103773)
  pythongh-103746: Test `types.UnionType` and `Literal` types together (python#103747)
  pythongh-103765: Fix 'Warning: py:class reference target not found: ModuleSpec' (pythonGH-103769)
  pythongh-87452: Improve the Popen.returncode docs
  Removed unnecessary escaping of asterisks (python#103714)
  pythonGH-102973: Slim down Fedora packages in the dev container (python#103283)
  pythongh-103091: Add PyUnstable_Type_AssignVersionTag (python#103095)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

4 participants