From f0eadd11b578799f1092028792a6b45bf4731317 Mon Sep 17 00:00:00 2001 From: mivanit Date: Sun, 18 Aug 2024 21:47:29 -0400 Subject: [PATCH 1/5] fix properties and cached_properties failing when adding git links see https://github.com/pdoc3/pdoc/issues/450 --- pdoc/html_helpers.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pdoc/html_helpers.py b/pdoc/html_helpers.py index f5e7e441..a384549c 100644 --- a/pdoc/html_helpers.py +++ b/pdoc/html_helpers.py @@ -8,7 +8,7 @@ import textwrap import traceback from contextlib import contextmanager -from functools import partial, lru_cache +from functools import partial, lru_cache, cached_property from typing import Callable, Match, Optional from warnings import warn import xml.etree.ElementTree as etree @@ -564,20 +564,28 @@ def format_git_link(template: str, dobj: pdoc.Doc): try: if 'commit' in _str_template_fields(template): commit = _git_head_commit() - abs_path = inspect.getfile(inspect.unwrap(dobj.obj)) + obj = dobj.obj + + # special handlers for properties and cached_properties + if isinstance(obj, property): + obj = obj.fget + elif isinstance(obj, cached_property): + obj = obj.func + + abs_path = inspect.getfile(inspect.unwrap(obj)) path = _project_relative_path(abs_path) # Urls should always use / instead of \\ if os.name == 'nt': path = path.replace('\\', '/') - lines, start_line = inspect.getsourcelines(dobj.obj) + lines, start_line = inspect.getsourcelines(obj) start_line = start_line or 1 # GH-296 end_line = start_line + len(lines) - 1 url = template.format(**locals()) return url except Exception: - warn(f'format_git_link for {dobj.obj} failed:\n{traceback.format_exc()}') + warn(f'format_git_link for {obj} failed:\n{traceback.format_exc()}') return None From 15d322f86d6bdda62613505b2cfd9928e5ad82da Mon Sep 17 00:00:00 2001 From: mivanit Date: Sun, 18 Aug 2024 21:54:48 -0400 Subject: [PATCH 2/5] fix whitespace for flake8 --- pdoc/html_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdoc/html_helpers.py b/pdoc/html_helpers.py index a384549c..9a04b982 100644 --- a/pdoc/html_helpers.py +++ b/pdoc/html_helpers.py @@ -571,7 +571,7 @@ def format_git_link(template: str, dobj: pdoc.Doc): obj = obj.fget elif isinstance(obj, cached_property): obj = obj.func - + abs_path = inspect.getfile(inspect.unwrap(obj)) path = _project_relative_path(abs_path) From 3ceaf8bbbb81bb8fff95c9fea10f5add44c261ee Mon Sep 17 00:00:00 2001 From: mivanit Date: Mon, 19 Aug 2024 00:34:41 -0400 Subject: [PATCH 3/5] special case for namedtuple fields --- pdoc/html_helpers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pdoc/html_helpers.py b/pdoc/html_helpers.py index 9a04b982..0da5025e 100644 --- a/pdoc/html_helpers.py +++ b/pdoc/html_helpers.py @@ -566,11 +566,15 @@ def format_git_link(template: str, dobj: pdoc.Doc): commit = _git_head_commit() obj = dobj.obj - # special handlers for properties and cached_properties + # special handlers for properties, cached_properties, and tuples if isinstance(obj, property): obj = obj.fget elif isinstance(obj, cached_property): obj = obj.func + elif hasattr(obj, '__class__') and obj.__class__.__name__ == '_tuplegetter': + # This is a NamedTuple field + class_name = dobj.qualname.rsplit('.', 1)[0] + obj = getattr(dobj.module.obj, class_name) abs_path = inspect.getfile(inspect.unwrap(obj)) path = _project_relative_path(abs_path) From 9f7a9f4d576b950f8c4470eae30f4975094f3ca7 Mon Sep 17 00:00:00 2001 From: mivanit Date: Mon, 19 Aug 2024 02:47:52 -0400 Subject: [PATCH 4/5] add special case for member_descriptor --- pdoc/html_helpers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pdoc/html_helpers.py b/pdoc/html_helpers.py index 0da5025e..838ae980 100644 --- a/pdoc/html_helpers.py +++ b/pdoc/html_helpers.py @@ -571,8 +571,10 @@ def format_git_link(template: str, dobj: pdoc.Doc): obj = obj.fget elif isinstance(obj, cached_property): obj = obj.func - elif hasattr(obj, '__class__') and obj.__class__.__name__ == '_tuplegetter': - # This is a NamedTuple field + elif ( + (hasattr(obj, '__class__') and obj.__class__.__name__ == '_tuplegetter') + or inspect.ismemberdescriptor(obj) + ): class_name = dobj.qualname.rsplit('.', 1)[0] obj = getattr(dobj.module.obj, class_name) From ce821a3f843600aa5152d448b398d8f702696cf1 Mon Sep 17 00:00:00 2001 From: Kernc Date: Fri, 13 Dec 2024 01:25:06 +0100 Subject: [PATCH 5/5] Extend existing _unwrap_descriptor() function --- pdoc/__init__.py | 21 ++++++++++++++++----- pdoc/html_helpers.py | 17 ++--------------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/pdoc/__init__.py b/pdoc/__init__.py index 5fd9a73c..28406d6c 100644 --- a/pdoc/__init__.py +++ b/pdoc/__init__.py @@ -19,9 +19,9 @@ import typing from contextlib import contextmanager from copy import copy -from functools import lru_cache, reduce, partial, wraps +from functools import cached_property, lru_cache, reduce, partial, wraps from itertools import tee, groupby -from types import ModuleType +from types import FunctionType, ModuleType from typing import ( # noqa: F401 cast, Any, Callable, Dict, Generator, Iterable, List, Literal, Mapping, NewType, Optional, Set, Tuple, Type, TypeVar, Union, @@ -421,11 +421,22 @@ def _is_descriptor(obj): inspect.ismemberdescriptor(obj)) -def _unwrap_descriptor(obj): +def _unwrap_descriptor(dobj): + obj = dobj.obj if isinstance(obj, property): return (getattr(obj, 'fget', False) or getattr(obj, 'fset', False) or getattr(obj, 'fdel', obj)) + if isinstance(obj, cached_property): + return obj.func + if isinstance(obj, FunctionType): + return obj + if (inspect.ismemberdescriptor(obj) or + getattr(getattr(obj, '__class__', 0), '__name__', 0) == '_tuplegetter'): + class_name = dobj.qualname.rsplit('.', 1)[0] + obj = getattr(dobj.module.obj, class_name) + return obj + # XXX: Follow descriptor protocol? Already proved buggy in conditions above return getattr(obj, '__get__', obj) @@ -550,7 +561,7 @@ def source(self) -> str: available, an empty string. """ try: - lines, _ = inspect.getsourcelines(_unwrap_descriptor(self.obj)) + lines, _ = inspect.getsourcelines(_unwrap_descriptor(self)) except (ValueError, TypeError, OSError): return '' return inspect.cleandoc(''.join(['\n'] + lines)) @@ -1402,7 +1413,7 @@ def return_annotation(self, *, link=None) -> str: # global variables lambda: _get_type_hints(not self.cls and self.module.obj)[self.name], # properties - lambda: inspect.signature(_unwrap_descriptor(self.obj)).return_annotation, + lambda: inspect.signature(_unwrap_descriptor(self)).return_annotation, # Use raw annotation strings in unmatched forward declarations lambda: cast(Class, self.cls).obj.__annotations__[self.name], # Extract annotation from the docstring for C builtin function diff --git a/pdoc/html_helpers.py b/pdoc/html_helpers.py index 838ae980..f16f1c16 100644 --- a/pdoc/html_helpers.py +++ b/pdoc/html_helpers.py @@ -8,7 +8,7 @@ import textwrap import traceback from contextlib import contextmanager -from functools import partial, lru_cache, cached_property +from functools import partial, lru_cache from typing import Callable, Match, Optional from warnings import warn import xml.etree.ElementTree as etree @@ -564,20 +564,7 @@ def format_git_link(template: str, dobj: pdoc.Doc): try: if 'commit' in _str_template_fields(template): commit = _git_head_commit() - obj = dobj.obj - - # special handlers for properties, cached_properties, and tuples - if isinstance(obj, property): - obj = obj.fget - elif isinstance(obj, cached_property): - obj = obj.func - elif ( - (hasattr(obj, '__class__') and obj.__class__.__name__ == '_tuplegetter') - or inspect.ismemberdescriptor(obj) - ): - class_name = dobj.qualname.rsplit('.', 1)[0] - obj = getattr(dobj.module.obj, class_name) - + obj = pdoc._unwrap_descriptor(dobj) abs_path = inspect.getfile(inspect.unwrap(obj)) path = _project_relative_path(abs_path)