From 7226d7df6e7a29d9ae5581a8bb41526f8ef444eb Mon Sep 17 00:00:00 2001 From: andrei kulakov Date: Wed, 27 Oct 2021 09:29:10 -0400 Subject: [PATCH 1/5] guard against access of classproperties --- Lib/inspect.py | 3 +++ Lib/pydoc.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Lib/inspect.py b/Lib/inspect.py index a956acf310f85b..b2b78e11894ee2 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -546,6 +546,9 @@ def classify_class_attrs(cls): try: if name == '__dict__': raise Exception("__dict__ is special, don't want the proxy") + o = cls.__dict__.get(name) + if isinstance(o, classmethod) and isinstance(o.__func__, property): + raise Exception("class property") get_obj = getattr(cls, name) except Exception as exc: pass diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 3a2ff218f8319a..e8391fd7277b7d 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -1368,6 +1368,9 @@ def spill(msg, attrs, predicate): push(msg) for name, kind, homecls, value in ok: try: + o = object.__dict__.get(name) + if isinstance(o, classmethod) and isinstance(o.__func__, property): + raise Exception("class property") value = getattr(object, name) except Exception: # Some descriptors may meet a failure in their __get__. From 4c78430990c06f3eb807fe6d243dcd9b09884faf Mon Sep 17 00:00:00 2001 From: andrei kulakov Date: Wed, 27 Oct 2021 10:13:17 -0400 Subject: [PATCH 2/5] fix help() and inspect.classify_class_attrs: prevent access of class properties --- Lib/inspect.py | 3 ++- Lib/pydoc.py | 3 ++- Lib/test/test_pydoc.py | 11 +++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index b2b78e11894ee2..e141f705a9c66d 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -547,7 +547,8 @@ def classify_class_attrs(cls): if name == '__dict__': raise Exception("__dict__ is special, don't want the proxy") o = cls.__dict__.get(name) - if isinstance(o, classmethod) and isinstance(o.__func__, property): + if isinstance(o, classmethod) and \ + isinstance(o.__func__, property): raise Exception("class property") get_obj = getattr(cls, name) except Exception as exc: diff --git a/Lib/pydoc.py b/Lib/pydoc.py index e8391fd7277b7d..3e8f81dc3fb0b7 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -1369,7 +1369,8 @@ def spill(msg, attrs, predicate): for name, kind, homecls, value in ok: try: o = object.__dict__.get(name) - if isinstance(o, classmethod) and isinstance(o.__func__, property): + if isinstance(o, classmethod) and \ + isinstance(o.__func__, property): raise Exception("class property") value = getattr(object, name) except Exception: diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py index 0a7d72c768424b..c70b9af0120e94 100644 --- a/Lib/test/test_pydoc.py +++ b/Lib/test/test_pydoc.py @@ -514,6 +514,17 @@ class object " | ... and \\d+ other subclasses") self.assertRegex(text, snip) + def test_classmethod_property(self): + # Issue 45356 + class A: + @classmethod + @property + def a(self): + # BaseException because accessor of attributes in pydoc and in + # inspect catches Exception + raise BaseException + pydoc.render_doc(A) + def test_builtin_with_child(self): """Tests help on builtin object which have only child classes. From d68d0a65939175d4bbeb6f4dd35aec3c32b0b3f7 Mon Sep 17 00:00:00 2001 From: andrei kulakov Date: Wed, 27 Oct 2021 15:44:25 -0400 Subject: [PATCH 3/5] also look at MRO and expand test accordingly --- Lib/inspect.py | 9 +++++---- Lib/pydoc.py | 9 +++++---- Lib/test/test_pydoc.py | 2 ++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index e141f705a9c66d..2bb74e6583c9fc 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -546,10 +546,11 @@ def classify_class_attrs(cls): try: if name == '__dict__': raise Exception("__dict__ is special, don't want the proxy") - o = cls.__dict__.get(name) - if isinstance(o, classmethod) and \ - isinstance(o.__func__, property): - raise Exception("class property") + for base in cls.__mro__: + o = base.__dict__.get(name) + if isinstance(o, classmethod) and \ + isinstance(o.__func__, property): + raise Exception("class property") get_obj = getattr(cls, name) except Exception as exc: pass diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 3e8f81dc3fb0b7..5e1cc9aafdd331 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -1368,10 +1368,11 @@ def spill(msg, attrs, predicate): push(msg) for name, kind, homecls, value in ok: try: - o = object.__dict__.get(name) - if isinstance(o, classmethod) and \ - isinstance(o.__func__, property): - raise Exception("class property") + for base in object.__mro__: + o = base.__dict__.get(name) + if isinstance(o, classmethod) and \ + isinstance(o.__func__, property): + raise Exception("class property") value = getattr(object, name) except Exception: # Some descriptors may meet a failure in their __get__. diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py index c70b9af0120e94..328f8719faace8 100644 --- a/Lib/test/test_pydoc.py +++ b/Lib/test/test_pydoc.py @@ -523,7 +523,9 @@ def a(self): # BaseException because accessor of attributes in pydoc and in # inspect catches Exception raise BaseException + class B(A): pass pydoc.render_doc(A) + pydoc.render_doc(B) def test_builtin_with_child(self): """Tests help on builtin object which have only child classes. From 3cec62fd708347d5246df5991390475234845416 Mon Sep 17 00:00:00 2001 From: andrei kulakov Date: Wed, 27 Oct 2021 15:52:53 -0400 Subject: [PATCH 4/5] factor out _is_class_property --- Lib/inspect.py | 15 ++++++++++----- Lib/pydoc.py | 9 ++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 2bb74e6583c9fc..542eeef79a5ab1 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -486,6 +486,14 @@ def getmembers(object, predicate=None): Attribute = namedtuple('Attribute', 'name kind defining_class object') +def _is_class_property(cls, name): + for base in cls.__mro__: + o = base.__dict__.get(name) + if isinstance(o, classmethod) and \ + isinstance(o.__func__, property): + return True + return False + def classify_class_attrs(cls): """Return list of attribute-descriptor tuples. @@ -546,11 +554,8 @@ def classify_class_attrs(cls): try: if name == '__dict__': raise Exception("__dict__ is special, don't want the proxy") - for base in cls.__mro__: - o = base.__dict__.get(name) - if isinstance(o, classmethod) and \ - isinstance(o.__func__, property): - raise Exception("class property") + if _is_class_property(cls, name): + raise Exception("class property") get_obj = getattr(cls, name) except Exception as exc: pass diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 5e1cc9aafdd331..617ddf57abf968 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -885,6 +885,8 @@ def spill(msg, attrs, predicate): push(msg) for name, kind, homecls, value in ok: try: + if inspect._is_class_property(object, name): + raise Exception("class property") value = getattr(object, name) except Exception: # Some descriptors may meet a failure in their __get__. @@ -1368,11 +1370,8 @@ def spill(msg, attrs, predicate): push(msg) for name, kind, homecls, value in ok: try: - for base in object.__mro__: - o = base.__dict__.get(name) - if isinstance(o, classmethod) and \ - isinstance(o.__func__, property): - raise Exception("class property") + if inspect._is_class_property(object, name): + raise Exception("class property") value = getattr(object, name) except Exception: # Some descriptors may meet a failure in their __get__. From 9c37c9c8edf03b52c71e6a886971ddd62e541b7d Mon Sep 17 00:00:00 2001 From: andrei kulakov Date: Thu, 28 Oct 2021 22:29:39 -0400 Subject: [PATCH 5/5] add news entry --- .../next/Library/2021-10-28-22-29-30.bpo-45356.jCCqdb.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2021-10-28-22-29-30.bpo-45356.jCCqdb.rst diff --git a/Misc/NEWS.d/next/Library/2021-10-28-22-29-30.bpo-45356.jCCqdb.rst b/Misc/NEWS.d/next/Library/2021-10-28-22-29-30.bpo-45356.jCCqdb.rst new file mode 100644 index 00000000000000..6fb8e9a60a4f30 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-10-28-22-29-30.bpo-45356.jCCqdb.rst @@ -0,0 +1,4 @@ +:mod:`pydoc` ``help`` command, :meth:`~pydoc.HTMLDoc.docclass`, +:meth:`~pydoc.TextDoc.docclass`, and :func:`~inspect.classify_class_attrs` +will no longer execute the logic of a class property when examining a class +object that contains such property.