Repository navigation
property assumes that its subclasses have __dict__ #98963
Description
Activity
- addedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
on Nov 1, 2022 Recommend ignoring the
AttributeError. Loss of a__doc__in this situation is of no consequence.Thinking about it more, I came to the opposite conclusion.
Ignoring
AttributeErrormoves the surprising behavior elsewhere – now people would wonder why doc isn't set as requested. (Worse, if thepropertysubclass is defined in a library, it's likely that a user would bump into this, rather than the library author.)The main issue here is that the error is surprising – it's not clear what's happening and what the right course of action is. If you're subclassing
propertyin the C API, adding a__doc__descriptor (or addingPy_TPFLAGS_MANAGED_DICT/tp_dictoffset) shouldn't be an issue – if you know that's what's necessary. (If losing__doc__is OK, the descriptor can even ignore the data and always getNone. Making it explicit sounds better in this case.)
So, another option is to just raise a better error – “property subclasses require a writable doc instance attribute”?Reacted by Erlend E. AaslandI also don't think that losing the
__doc__silently is a good behavior.Would it not be an option to relax the condition at the top? Instead of
Py_IS_TYPE(self, &PyProperty_Type), the test could spend some more time to see when it is safe to usePyObject_SetAttr. AFAIK that would be- If the type has an associated dictionary
- If somewhere in the type hierarchy, we expose a writable
__doc__field viatp_members
- added a commit that references this issue
on Nov 2, 2022 If somewhere in the type hierarchy, we expose a writable
__doc__field viatp_memberspropertydoes that. But Python doesn't consider the superclass's descriptor (even when setting it) when there's a class attribute of the same name on the subclass. Here, that's the subclass's own docstring (Noneby default).To make
__doc__work, it needs to either be a descriptor on the subclass, or an attribute on the instance.Reacted by Wenzel JakobFWIW,. this code has been stable for 20 years. AFAICT, the only motivation for an edit is to accommodate single, new, and somewhat exotic C extension. So any proposed change should be minimally invasive.
Thinking about it more, I came to the opposite conclusion.
It doesn't feel like my assessment is being valued.
any proposed change should be minimally invasive.
I definitely agree with that! I hope to only touch the error case.
AFAICT, the only motivation for an edit is to accommodate single, new, and somewhat exotic C extension.
My opinion here is more nuanced though. The C API is not used only with C, and I've been trying to make it more useful with other languages (C++, Rust, Java). Language bindings work at a lower level than typical C extensions, so they run into subtle bugs we haven't seen before.
So, yes, it's a somewhat exotic C extension – but if we fix the issues it encounters, we'll hopefully get more extensions like it, and it'll become less exotic.It doesn't feel like my assessment is being valued.
I'm sorry I made you feel this way. I tried to react to you and explain my reasoning. How can I do better?
Reacted by Erlend E. Aasland and C.A.M. GerlachFor what it's worth, I understand @rhettinger's concern that this is somewhat of a niche issue. While making Python behave more as expected is desirable, I would already be very happy simply with guidance on how to work around this issue in a stable manner (meaning that it works for past Python versions, and that I can have some reasonable assurance it won't break in the future).
Alas, that requirement of stability is what is making things very difficult:
-
@encukou suggested that the subclass should expose a writeable
__doc__member. However, the behavior of such a combination is unstable across Python versions. A minimal C-API reproducer can be found here: https://github.com/wjakob/inheritance_issue_2/blob/master/inheritance_issue_2.cTo try:
$ pip install git+https://github.com/wjakob/inheritance_issue_2
In Python 3.10
>>> import inheritance_issue_2 >>> print(inheritance_issue_2.my_property(lambda x: None, doc='test').__doc__) None
In Python 3.12 (main)
>>> import inheritance_issue_2 >>> print(inheritance_issue_2.my_property(lambda x: None, doc='test').__doc__) test
So the point by @rhettinger (this code has been stable for 20 years) does not seem to hold 100%. Something has subtly changed.
-
Py_TPFLAGS_MANAGED_DICTsuggested by @encukou is not in the limited API, which I require at least as an optional build configuration. So that one is out. -
Installing an unmanaged dictionary via
__dictoffset__is unstable across Python versions. A minimal C-API reproducer can be found here: https://github.com/wjakob/inheritance_issue_3/blob/master/inheritance_issue_3.cTo try:
$ pip install git+https://github.com/wjakob/inheritance_issue_3
In Python 3.10
>>> import inheritance_issue_3 >>> print(inheritance_issue_3.my_property(lambda x: None, doc='test').__doc__) None
In Python 3.12 (main)
>>> import inheritance_issue_3 >>> print(inheritance_issue_3.my_property(lambda x: None, doc='test').__doc__) test
-
The only thing I have discovered so far which consistently works across Python versions is to set the new property type's
tp_memberspointer toPyProperty_Type.tp_members. This can be done even in the limited API viaPyType_GetSlot, but as @encukou pointed out this makes too many assumptions about the underlying implementation. So although it happens to compile and work with-DPy_LIMITED_APIright now, it is not future-proof.
If you have advice on other things to try, I am all ears.. :)
Reacted by Erlend E. Aasland-
- added a commit that references this issue
on Nov 3, 2022 Aha! It turns out that this was not stable -- in fact this was just changed for 3.12: #23205
(If anything, this underscores the point that touching old code should be done extremely carefully.)@wjakob: There's even some pybind11 fallout: pybind/pybind11#4168
Given that silencing the(Edit: no, the pre-3.12 behavior was more complicated: the error was raised or not depending on where the docstring came from.)AttributeErrorwould essentially revert this case to pre-3.12 behavior, it would make sense.
But, before I wrote this I already wrote a patch for improving the error message, so I'll post it here: #99058Reacted by Gregory P. SmithAha, it's great to know that v3.12 is the place where this change happened. Incidentally, this is also the first Python version for which
-DPy_LIMITED_APIwill be complete enough for my project.So what I am thinking to do now as a workaround is:
-
For Python < 3.12, set the new property's
tp_membersfield toPyProperty_Type.tp_members. -
For Python >= 3.12 and in particular limited API mode, declare an independent
tp_membersfield that adds a__doc__field. (That seems more economical than a full-blown dictionary)
The error message in PR #99058 looks good to me.
-
- added a commit that references this issue
on Nov 3, 2022 Actually, the Python 3.12 change was for the
docargument – when the docstring is being copied from a setter function, instantiation does fail as far back as Python 2. Pre-3.12, thedocargument doesn't work on subclasses at all.1 remaining item
This came up in protobuf because it has a
class _FieldProperty(property): __slots__ = ("DESCRIPTOR",)
calling
property.__init__()on such a class instance fails with an exception, no matter what args are passed, doc or not:property.__init__(self, getter, setter, doc=doc) AttributeError: '_FieldProperty' object attribute '__doc__' is read-only
If
"__doc__"is added to the__slots__tuple, it works again.In older versions of Python the the property constructor silently does not assign a docstring rather than causing an
AttributeError.That is narrower than the larger issue here, but I think we should mirror that silent doc-dropping behavior for 3.12 and consider if we're deprecating that behavior later or just want to make it official.
- added a commit that references this issue
on Jun 3, 2023 That is narrower than the larger issue here
re-reading things, I don't think that is true. This issue title talks about a dict, but the issue is entirely about a the docstring being unsettable and whether that is ignored as it always has been or is an expected behavior changing error.
Existing code depends on it being ignored.
closing as the 3.12 PR is set to automerge; previous exception / ignored error behavior has been restored.
- added a commit that references this issue
on Jun 5, 2023 Related to this in general... CPython <=3.11 has some seriously smelly behavior:
Python 3.10.9 (main, Dec 7 2022, 13:47:07) [GCC 12.2.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> class slotted_property(property): ... __slots__ = () ... >>> slotted_property() <__main__.slotted_property object at 0x7f36e189dad0> >>> slotted_property(lambda x: None) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'slotted_property' object attribute '__doc__' is read-only >>> slotted_property(lambda x: None, doc='ignored').__doc__ >>> slotted_property(lambda x: None, doc='').__doc__ >>> slotted_property(lambda x: None, doc=None).__doc__ Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'slotted_property' object attribute '__doc__' is read-only >>>
Those surprisingly inconsistent
AttributeErrors are gone in 3.12. If we ever decide to bring an exception back in this case, we'll want to be fully consistent about it.Thank you for taking this on!
A thanks from me as well. With your change, I was able to remove the Python 3.12-specific workaround/hack from nanobind.
Reacted by Gregory P. Smith
property'stp_inithas this code:This assumes that subclasses of
propertyhave a__dict__(or a__doc__attribute settable by other means). That is the case when subclassed using theclassstatement, but might not be true using C.A C-API reproducer is at https://github.com/wjakob/inheritance_issue/blob/master/inheritance_issue.c
I don't see a good way to fix this. We could:
Py_TPFLAGS_MANAGED_DICT(or provide a__doc__descriptor), orAttributeError, unless__doc__was set explicitly?