Skip to content

Commit 3e7ddc2

Browse files
[3.12] gh-98963: Restore the ability to have a dict-less property. (GH-105262) (#105297)
gh-98963: Restore the ability to have a dict-less property. (GH-105262) Ignore doc string assignment failures in `property` as has been the behavior of all past Python releases. (the docstring is discarded) (cherry picked from commit 418befd) This fixes a behavior regression in 3.12beta1 where an AttributeError was being raised in a situation it has never been in the past. It keeps the existing unusual single situation where AttributeError does get raised. Existing widely deployed projects depend on this not raising an exception. Co-authored-by: Gregory P. Smith <[email protected]>
1 parent 9ce3312 commit 3e7ddc2

File tree

3 files changed

+97
-13
lines changed

3 files changed

+97
-13
lines changed

Lib/test/test_property.py

+56-5
Original file line numberDiff line numberDiff line change
@@ -246,16 +246,67 @@ class PropertySubSlots(property):
246246
class PropertySubclassTests(unittest.TestCase):
247247

248248
def test_slots_docstring_copy_exception(self):
249-
try:
249+
# A special case error that we preserve despite the GH-98963 behavior
250+
# that would otherwise silently ignore this error.
251+
# This came from commit b18500d39d791c879e9904ebac293402b4a7cd34
252+
# as part of https://bugs.python.org/issue5890 which allowed docs to
253+
# be set via property subclasses in the first place.
254+
with self.assertRaises(AttributeError):
250255
class Foo(object):
251256
@PropertySubSlots
252257
def spam(self):
253258
"""Trying to copy this docstring will raise an exception"""
254259
return 1
255-
except AttributeError:
256-
pass
257-
else:
258-
raise Exception("AttributeError not raised")
260+
261+
def test_property_with_slots_no_docstring(self):
262+
# https://github.com/python/cpython/issues/98963#issuecomment-1574413319
263+
class slotted_prop(property):
264+
__slots__ = ("foo",)
265+
266+
p = slotted_prop() # no AttributeError
267+
self.assertIsNone(getattr(p, "__doc__", None))
268+
269+
def undocumented_getter():
270+
return 4
271+
272+
p = slotted_prop(undocumented_getter) # New in 3.12: no AttributeError
273+
self.assertIsNone(getattr(p, "__doc__", None))
274+
275+
@unittest.skipIf(sys.flags.optimize >= 2,
276+
"Docstrings are omitted with -O2 and above")
277+
def test_property_with_slots_docstring_silently_dropped(self):
278+
# https://github.com/python/cpython/issues/98963#issuecomment-1574413319
279+
class slotted_prop(property):
280+
__slots__ = ("foo",)
281+
282+
p = slotted_prop(doc="what's up") # no AttributeError
283+
self.assertIsNone(p.__doc__)
284+
285+
def documented_getter():
286+
"""getter doc."""
287+
return 4
288+
289+
# Historical behavior: A docstring from a getter always raises.
290+
# (matches test_slots_docstring_copy_exception above).
291+
with self.assertRaises(AttributeError):
292+
p = slotted_prop(documented_getter)
293+
294+
@unittest.skipIf(sys.flags.optimize >= 2,
295+
"Docstrings are omitted with -O2 and above")
296+
def test_property_with_slots_and_doc_slot_docstring_present(self):
297+
# https://github.com/python/cpython/issues/98963#issuecomment-1574413319
298+
class slotted_prop(property):
299+
__slots__ = ("foo", "__doc__")
300+
301+
p = slotted_prop(doc="what's up")
302+
self.assertEqual("what's up", p.__doc__) # new in 3.12: This gets set.
303+
304+
def documented_getter():
305+
"""what's up getter doc?"""
306+
return 4
307+
308+
p = slotted_prop(documented_getter)
309+
self.assertEqual("what's up getter doc?", p.__doc__)
259310

260311
@unittest.skipIf(sys.flags.optimize >= 2,
261312
"Docstrings are omitted with -O2 and above")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Restore the ability for a subclass of :class:`property` to define ``__slots__``
2+
or otherwise be dict-less by ignoring failures to set a docstring on such a
3+
class. This behavior had regressed in 3.12beta1. An :exc:`AttributeError`
4+
where there had not previously been one was disruptive to existing code.

Objects/descrobject.c

+37-8
Original file line numberDiff line numberDiff line change
@@ -1485,7 +1485,10 @@ class property(object):
14851485
self.__get = fget
14861486
self.__set = fset
14871487
self.__del = fdel
1488-
self.__doc__ = doc
1488+
try:
1489+
self.__doc__ = doc
1490+
except AttributeError: # read-only or dict-less class
1491+
pass
14891492
14901493
def __get__(self, inst, type=None):
14911494
if inst is None:
@@ -1791,6 +1794,19 @@ property_init_impl(propertyobject *self, PyObject *fget, PyObject *fset,
17911794
if (rc <= 0) {
17921795
return rc;
17931796
}
1797+
if (!Py_IS_TYPE(self, &PyProperty_Type) &&
1798+
prop_doc != NULL && prop_doc != Py_None) {
1799+
// This oddity preserves the long existing behavior of surfacing
1800+
// an AttributeError when using a dict-less (__slots__) property
1801+
// subclass as a decorator on a getter method with a docstring.
1802+
// See PropertySubclassTest.test_slots_docstring_copy_exception.
1803+
int err = PyObject_SetAttr(
1804+
(PyObject *)self, &_Py_ID(__doc__), prop_doc);
1805+
if (err < 0) {
1806+
Py_DECREF(prop_doc); // release our new reference.
1807+
return -1;
1808+
}
1809+
}
17941810
if (prop_doc == Py_None) {
17951811
prop_doc = NULL;
17961812
Py_DECREF(Py_None);
@@ -1806,19 +1822,32 @@ property_init_impl(propertyobject *self, PyObject *fget, PyObject *fset,
18061822
if (Py_IS_TYPE(self, &PyProperty_Type)) {
18071823
Py_XSETREF(self->prop_doc, prop_doc);
18081824
} else {
1809-
/* If this is a property subclass, put __doc__
1810-
in dict of the subclass instance instead,
1811-
otherwise it gets shadowed by __doc__ in the
1812-
class's dict. */
1825+
/* If this is a property subclass, put __doc__ in the dict
1826+
or designated slot of the subclass instance instead, otherwise
1827+
it gets shadowed by __doc__ in the class's dict. */
18131828

18141829
if (prop_doc == NULL) {
18151830
prop_doc = Py_NewRef(Py_None);
18161831
}
18171832
int err = PyObject_SetAttr(
18181833
(PyObject *)self, &_Py_ID(__doc__), prop_doc);
1819-
Py_XDECREF(prop_doc);
1820-
if (err < 0)
1821-
return -1;
1834+
Py_DECREF(prop_doc);
1835+
if (err < 0) {
1836+
assert(PyErr_Occurred());
1837+
if (PyErr_ExceptionMatches(PyExc_AttributeError)) {
1838+
PyErr_Clear();
1839+
// https://github.com/python/cpython/issues/98963#issuecomment-1574413319
1840+
// Python silently dropped this doc assignment through 3.11.
1841+
// We preserve that behavior for backwards compatibility.
1842+
//
1843+
// If we ever want to deprecate this behavior, only raise a
1844+
// warning or error when proc_doc is not None so that
1845+
// property without a specific doc= still works.
1846+
return 0;
1847+
} else {
1848+
return -1;
1849+
}
1850+
}
18221851
}
18231852

18241853
return 0;

0 commit comments

Comments
 (0)