Skip to content

Commit

Permalink
Merge pull request #389 from mhils/typeddict
Browse files Browse the repository at this point in the history
Improve TypedDict rendering
  • Loading branch information
mhils authored May 4, 2022
2 parents a57cdb2 + c876a0f commit f6479f1
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 124 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

# Unreleased: pdoc next

- Improve rendering of `typing.TypedDict` subclasses.
([#389](https://github.com/mitmproxy/pdoc/issues/389), [@mhils](https://github.com/mhils))


# 2022-04-24: pdoc 11.1.0

- Display line numbers when viewing source code.
Expand Down
2 changes: 1 addition & 1 deletion pdoc/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
default="restructuredtext",
choices=("markdown", "google", "numpy", "restructuredtext"),
help="The default docstring format. For non-Markdown formats, pdoc will first convert matching syntax elements to "
"Markdown and then process everything as Markdown.",
"Markdown and then process everything as Markdown.",
)
renderopts.add_argument(
"-e",
Expand Down
87 changes: 50 additions & 37 deletions pdoc/doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,15 +215,15 @@ def _member_objects(self) -> dict[str, Any]:
def _var_docstrings(self) -> dict[str, str]:
"""A mapping from some member variable names to their docstrings."""

@abstractmethod
def _taken_from(self, member_name: str, obj: Any) -> tuple[str, str]:
"""The location this member was taken from. If unknown, (modulename, qualname) is returned."""

@cached_property
@abstractmethod
def _var_annotations(self) -> dict[str, Any]:
"""A mapping from some member variable names to their type annotations."""

@abstractmethod
def _taken_from(self, member_name: str, obj: Any) -> tuple[str, str]:
"""The location this member was taken from. If unknown, (modulename, qualname) is returned."""

@cached_property
@abstractmethod
def own_members(self) -> list[Doc]:
Expand Down Expand Up @@ -394,6 +394,14 @@ def is_package(self) -> bool:
def _var_docstrings(self) -> dict[str, str]:
return doc_ast.walk_tree(self.obj).docstrings

@cached_property
def _var_annotations(self) -> dict[str, Any]:
annotations = doc_ast.walk_tree(self.obj).annotations.copy()
for k, v in _safe_getattr(self.obj, "__annotations__", {}).items():
annotations[k] = v

return resolve_annotations(annotations, self.obj, self.fullname)

def _taken_from(self, member_name: str, obj: Any) -> tuple[str, str]:
if obj is empty:
return self.modulename, f"{self.qualname}.{member_name}".lstrip(".")
Expand All @@ -410,14 +418,6 @@ def _taken_from(self, member_name: str, obj: Any) -> tuple[str, str]:
"."
)

@cached_property
def _var_annotations(self) -> dict[str, Any]:
annotations = doc_ast.walk_tree(self.obj).annotations.copy()
for k, v in _safe_getattr(self.obj, "__annotations__", {}).items():
annotations[k] = v

return resolve_annotations(annotations, self.obj, self.fullname)

@cached_property
def own_members(self) -> list[Doc]:
return list(self.members.values())
Expand Down Expand Up @@ -544,6 +544,15 @@ class Class(Namespace[type]):
def __repr__(self):
return f"<{_decorators(self)}class {self.modulename}.{self.qualname}{_docstr(self)}{_children(self)}>"

@cached_property
def docstring(self) -> str:
doc = Doc.docstring.__get__(self) # type: ignore
if doc == dict.__doc__:
# Don't display default docstring for dict subclasses (primarily TypedDict).
return ""
else:
return doc

@cached_property
def _var_docstrings(self) -> dict[str, str]:
docstrings: dict[str, str] = {}
Expand All @@ -552,31 +561,6 @@ def _var_docstrings(self) -> dict[str, str]:
docstrings.setdefault(name, docstr)
return docstrings

@cached_property
def _declarations(self) -> dict[str, tuple[str, str]]:
decls: dict[str, tuple[str, str]] = {}
for cls in self.obj.__mro__:
treeinfo = doc_ast.walk_tree(cls)
for name in treeinfo.docstrings.keys() | treeinfo.annotations.keys():
decls.setdefault(name, (cls.__module__, f"{cls.__qualname__}.{name}"))
for name in cls.__dict__:
decls.setdefault(name, (cls.__module__, f"{cls.__qualname__}.{name}"))
if decls.get("__init__", None) == ("builtins", "object.__init__"):
decls["__init__"] = (
self.obj.__module__,
f"{self.obj.__qualname__}.__init__",
)
return decls

def _taken_from(self, member_name: str, obj: Any) -> tuple[str, str]:
try:
return self._declarations[member_name]
except KeyError: # pragma: no cover
warnings.warn(
f"Cannot determine where {self.fullname}.{member_name} is taken from, assuming current file."
)
return self.modulename, f"{self.qualname}.{member_name}"

@cached_property
def _var_annotations(self) -> dict[str, type]:
# this is a bit tricky: __annotations__ also includes annotations from parent classes,
Expand Down Expand Up @@ -608,6 +592,32 @@ def _var_annotations(self) -> dict[str, type]:

return {k: v[1] for k, v in annotations.items()}

@cached_property
def _declarations(self) -> dict[str, tuple[str, str]]:
decls: dict[str, tuple[str, str]] = {}
for cls in self.obj.__mro__:
treeinfo = doc_ast.walk_tree(cls)
for name in treeinfo.docstrings.keys() | treeinfo.annotations.keys():
decls.setdefault(name, (cls.__module__, f"{cls.__qualname__}.{name}"))
for name in cls.__dict__:
decls.setdefault(name, (cls.__module__, f"{cls.__qualname__}.{name}"))
if decls.get("__init__", None) == ("builtins", "object.__init__"):
decls["__init__"] = (
self.obj.__module__,
f"{self.obj.__qualname__}.__init__",
)
return decls

def _taken_from(self, member_name: str, obj: Any) -> tuple[str, str]:
try:
return self._declarations[member_name]
except KeyError: # pragma: no cover
# TypedDict botches __mro__ and may need special casing here.
warnings.warn(
f"Cannot determine where {self.fullname}.{member_name} is taken from, assuming current file."
)
return self.modulename, f"{self.qualname}.{member_name}"

@cached_property
def own_members(self) -> list[Doc]:
members = self._members_by_origin.get((self.modulename, self.qualname), [])
Expand Down Expand Up @@ -639,6 +649,9 @@ def _member_objects(self) -> dict[str, Any]:
# Special case: Do not show a constructor for enums. They are typically not constructed by users.
# The alternative would be showing __new__, as __call__ is too verbose.
del unsorted["__init__"]
elif issubclass(self.obj, dict):
# Special case: Do not show a constructor for dict subclasses.
del unsorted["__init__"]
else:
# Check if there's a helpful Metaclass.__call__ or Class.__new__. This dance is very similar to
# https://github.com/python/cpython/blob/9feae41c4f04ca27fd2c865807a5caeb50bf4fc4/Lib/inspect.py#L2359-L2376
Expand Down
2 changes: 1 addition & 1 deletion pdoc/doc_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
import typing
import warnings
from types import BuiltinFunctionType, ModuleType
from typing import Any, TYPE_CHECKING
from typing import _GenericAlias # type: ignore
from typing import TYPE_CHECKING, Any

from . import extract
from ._compat import GenericAlias, Literal, UnionType, get_origin
Expand Down
4 changes: 3 additions & 1 deletion pdoc/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@

def configure(
*,
docformat: Literal["markdown", "google", "numpy", "restructuredtext"] = "restructuredtext",
docformat: Literal[
"markdown", "google", "numpy", "restructuredtext"
] = "restructuredtext",
edit_url_map: Mapping[str, str] | None = None,
favicon: str | None = None,
footer_text: str = "",
Expand Down
Loading

0 comments on commit f6479f1

Please sign in to comment.