pdoc 14.1.0__tar.gz → 14.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pdoc-14.1.0 → pdoc-14.2.0}/CHANGELOG.md +16 -0
- {pdoc-14.1.0/pdoc.egg-info → pdoc-14.2.0}/PKG-INFO +1 -1
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/__init__.py +1 -1
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/__main__.py +1 -1
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/_compat.py +21 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/doc.py +43 -11
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/doc_ast.py +8 -1
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/doc_types.py +30 -3
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/docstrings.py +4 -2
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/extract.py +61 -25
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/search.py +5 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/default/module.html.jinja2 +1 -0
- {pdoc-14.1.0 → pdoc-14.2.0/pdoc.egg-info}/PKG-INFO +1 -1
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc.egg-info/requires.txt +1 -1
- {pdoc-14.1.0 → pdoc-14.2.0}/pyproject.toml +3 -5
- {pdoc-14.1.0 → pdoc-14.2.0}/test/test_doc.py +32 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/test/test_doc_types.py +36 -8
- {pdoc-14.1.0 → pdoc-14.2.0}/test/test_extract.py +8 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/test/test_snapshot.py +5 -3
- {pdoc-14.1.0 → pdoc-14.2.0}/LICENSE +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/MANIFEST.in +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/README.md +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/doc_pyi.py +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/markdown2/LICENSE +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/markdown2/README.md +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/markdown2/__init__.py +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/py.typed +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/render.py +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/render_helpers.py +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/README.md +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/build-search-index.js +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/content.css +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/custom.css +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/default/error.html.jinja2 +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/default/frame.html.jinja2 +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/default/index.html.jinja2 +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/deprecated/README.md +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/deprecated/bootstrap-reboot.min.css +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/deprecated/box-arrow-in-left.svg +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/deprecated/elasticlunr.min.js +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/deprecated/favicon.svg +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/deprecated/navtoggle.svg +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/deprecated/pdoc-logo.svg +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/deprecated/resources/favicon.svg +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/layout.css +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/livereload.html.jinja2 +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/math.html.jinja2 +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/mermaid.html.jinja2 +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/resources/bootstrap-reboot.min.css +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/resources/box-arrow-in-left.svg +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/resources/elasticlunr.min.js +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/resources/exclamation-triangle-fill.svg +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/resources/info-circle-fill.svg +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/resources/lightning-fill.svg +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/resources/navtoggle.svg +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/resources/pdoc-logo.svg +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/search.html.jinja2 +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/search.js.jinja2 +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/syntax-highlighting.css +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/templates/theme.css +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc/web.py +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc.egg-info/SOURCES.txt +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc.egg-info/dependency_links.txt +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc.egg-info/entry_points.txt +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/pdoc.egg-info/top_level.txt +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/setup.cfg +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/test/test_doc_ast.py +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/test/test_doc_pyi.py +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/test/test_docstrings.py +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/test/test_main.py +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/test/test_render_helpers.py +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/test/test_search.py +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/test/test_smoke.py +0 -0
- {pdoc-14.1.0 → pdoc-14.2.0}/test/test_web.py +0 -0
@@ -4,6 +4,22 @@
|
|
4
4
|
|
5
5
|
<!-- ✨ You do not need to add a pull request reference or an author, this will be added automatically by CI. ✨ -->
|
6
6
|
|
7
|
+
## 2023-12-13: pdoc 14.2.0
|
8
|
+
|
9
|
+
- pdoc now documents PyO3 or pybind11 submodules that are not picked up by Python's builtin pkgutil module.
|
10
|
+
([#633](https://github.com/mitmproxy/pdoc/issues/633), @mhils)
|
11
|
+
- pdoc now supports Python 3.12's `type` statements and has improved `TypeAlias` rendering.
|
12
|
+
([#651](https://github.com/mitmproxy/pdoc/pull/651), @mhils)
|
13
|
+
- Imports in a TYPE_CHECKING section that reference members defined in another module's TYPE_CHECKING section now work
|
14
|
+
correctly.
|
15
|
+
([#649](https://github.com/mitmproxy/pdoc/pull/649), @mhils)
|
16
|
+
- Add support for `code-block` ReST directives
|
17
|
+
([#624](https://github.com/mitmproxy/pdoc/pull/624), @JCGoran)
|
18
|
+
- If a variable's value meets certain entropy criteria and matches an environment variable value,
|
19
|
+
pdoc will now emit a warning and display the variable's name as a placeholder instead.
|
20
|
+
This heuristic is meant to prevent accidental leakage of environment secrets and can be disabled by setting
|
21
|
+
`PDOC_DISPLAY_ENV_VARS=1`.
|
22
|
+
([#622](https://github.com/mitmproxy/pdoc/pull/622), @mhils)
|
7
23
|
|
8
24
|
## 2023-09-10: pdoc 14.1.0
|
9
25
|
|
@@ -462,7 +462,7 @@ You can find an example in [`examples/library-usage`](https://github.com/mitmpro
|
|
462
462
|
from __future__ import annotations
|
463
463
|
|
464
464
|
__docformat__ = "markdown" # explicitly disable rST processing in the examples above.
|
465
|
-
__version__ = "14.
|
465
|
+
__version__ = "14.2.0" # this is read from setup.py
|
466
466
|
|
467
467
|
from pathlib import Path
|
468
468
|
from typing import overload
|
@@ -112,7 +112,7 @@ renderopts.add_argument(
|
|
112
112
|
"--search",
|
113
113
|
action=BooleanOptionalAction,
|
114
114
|
default=True,
|
115
|
-
help="Enable search functionality.",
|
115
|
+
help="Enable search functionality if multiple modules are documented.",
|
116
116
|
)
|
117
117
|
renderopts.add_argument(
|
118
118
|
"--show-source",
|
@@ -16,6 +16,24 @@ else: # pragma: no cover
|
|
16
16
|
def ast_unparse(t): # type: ignore
|
17
17
|
return _unparse(t).strip("\t\n \"'")
|
18
18
|
|
19
|
+
if sys.version_info >= (3, 12):
|
20
|
+
from ast import TypeAlias as ast_TypeAlias
|
21
|
+
else: # pragma: no cover
|
22
|
+
class ast_TypeAlias:
|
23
|
+
pass
|
24
|
+
|
25
|
+
if sys.version_info >= (3, 12):
|
26
|
+
from typing import TypeAliasType
|
27
|
+
else: # pragma: no cover
|
28
|
+
class TypeAliasType:
|
29
|
+
"""Placeholder class for TypeAliasType"""
|
30
|
+
|
31
|
+
if sys.version_info >= (3, 10):
|
32
|
+
from typing import TypeAlias
|
33
|
+
else: # pragma: no cover
|
34
|
+
class TypeAlias:
|
35
|
+
pass
|
36
|
+
|
19
37
|
if sys.version_info >= (3, 9):
|
20
38
|
from types import GenericAlias
|
21
39
|
else: # pragma: no cover
|
@@ -108,6 +126,9 @@ else: # pragma: no cover
|
|
108
126
|
__all__ = [
|
109
127
|
"cache",
|
110
128
|
"ast_unparse",
|
129
|
+
"ast_TypeAlias",
|
130
|
+
"TypeAliasType",
|
131
|
+
"TypeAlias",
|
111
132
|
"GenericAlias",
|
112
133
|
"UnionType",
|
113
134
|
"removesuffix",
|
@@ -28,7 +28,6 @@ from functools import wraps
|
|
28
28
|
import inspect
|
29
29
|
import os
|
30
30
|
from pathlib import Path
|
31
|
-
import pkgutil
|
32
31
|
import re
|
33
32
|
import sys
|
34
33
|
import textwrap
|
@@ -45,15 +44,16 @@ import warnings
|
|
45
44
|
from pdoc import doc_ast
|
46
45
|
from pdoc import doc_pyi
|
47
46
|
from pdoc import extract
|
47
|
+
from pdoc._compat import TypeAlias
|
48
|
+
from pdoc._compat import TypeAliasType
|
49
|
+
from pdoc._compat import cache
|
50
|
+
from pdoc._compat import formatannotation
|
48
51
|
from pdoc.doc_types import GenericAlias
|
49
52
|
from pdoc.doc_types import NonUserDefinedCallables
|
50
53
|
from pdoc.doc_types import empty
|
51
54
|
from pdoc.doc_types import resolve_annotations
|
52
55
|
from pdoc.doc_types import safe_eval_type
|
53
56
|
|
54
|
-
from ._compat import cache
|
55
|
-
from ._compat import formatannotation
|
56
|
-
|
57
57
|
|
58
58
|
def _include_fullname_in_traceback(f):
|
59
59
|
"""
|
@@ -454,9 +454,6 @@ class Module(Namespace[types.ModuleType]):
|
|
454
454
|
@cached_property
|
455
455
|
def submodules(self) -> list[Module]:
|
456
456
|
"""A list of all (direct) submodules."""
|
457
|
-
if not self.is_package:
|
458
|
-
return []
|
459
|
-
|
460
457
|
include: Callable[[str], bool]
|
461
458
|
mod_all = _safe_getattr(self.obj, "__all__", False)
|
462
459
|
if mod_all is not False:
|
@@ -471,9 +468,8 @@ class Module(Namespace[types.ModuleType]):
|
|
471
468
|
# (think of OS-specific modules, e.g. _linux.py failing to import on Windows).
|
472
469
|
return not name.startswith("_")
|
473
470
|
|
474
|
-
submodules = []
|
475
|
-
for mod in
|
476
|
-
_, _, mod_name = mod.name.rpartition(".")
|
471
|
+
submodules: list[Module] = []
|
472
|
+
for mod_name, mod in extract.iter_modules2(self.obj).items():
|
477
473
|
if not include(mod_name):
|
478
474
|
continue
|
479
475
|
try:
|
@@ -1006,7 +1002,9 @@ class Function(Doc[types.FunctionType]):
|
|
1006
1002
|
)
|
1007
1003
|
)
|
1008
1004
|
for p in sig.parameters.values():
|
1009
|
-
p._annotation = safe_eval_type(
|
1005
|
+
p._annotation = safe_eval_type( # type: ignore
|
1006
|
+
p.annotation, globalns, localns, mod, self.fullname
|
1007
|
+
)
|
1010
1008
|
return sig
|
1011
1009
|
|
1012
1010
|
@cached_property
|
@@ -1092,6 +1090,11 @@ class Variable(Doc[None]):
|
|
1092
1090
|
else:
|
1093
1091
|
return False
|
1094
1092
|
|
1093
|
+
@cached_property
|
1094
|
+
def is_type_alias_type(self) -> bool:
|
1095
|
+
"""`True` if the variable is a `typing.TypeAliasType`, `False` otherwise."""
|
1096
|
+
return isinstance(self.default_value, TypeAliasType)
|
1097
|
+
|
1095
1098
|
@cached_property
|
1096
1099
|
def is_enum_member(self) -> bool:
|
1097
1100
|
"""`True` if the variable is an enum member, `False` otherwise."""
|
@@ -1105,6 +1108,27 @@ class Variable(Doc[None]):
|
|
1105
1108
|
"""The variable's default value as a pretty-printed str."""
|
1106
1109
|
if self.default_value is empty:
|
1107
1110
|
return ""
|
1111
|
+
if isinstance(self.default_value, TypeAliasType):
|
1112
|
+
return formatannotation(self.default_value.__value__)
|
1113
|
+
elif self.annotation == TypeAlias:
|
1114
|
+
return formatannotation(self.default_value)
|
1115
|
+
|
1116
|
+
# This is not perfect, but a solid attempt at preventing accidental leakage of secrets.
|
1117
|
+
# If you have input on how to improve the heuristic, please send a pull request!
|
1118
|
+
value_taken_from_env_var = (
|
1119
|
+
isinstance(self.default_value, str)
|
1120
|
+
and len(self.default_value) >= 8
|
1121
|
+
and self.default_value in _environ_lookup()
|
1122
|
+
)
|
1123
|
+
if value_taken_from_env_var and not os.environ.get("PDOC_DISPLAY_ENV_VARS", ""):
|
1124
|
+
env_var = "$" + _environ_lookup()[self.default_value]
|
1125
|
+
warnings.warn(
|
1126
|
+
f"The default value of {self.fullname} matches the {env_var} environment variable. "
|
1127
|
+
f"To prevent accidental leakage of secrets, the default value is not displayed. "
|
1128
|
+
f"Disable this behavior by setting PDOC_DISPLAY_ENV_VARS=1 as an environment variable.",
|
1129
|
+
RuntimeWarning,
|
1130
|
+
)
|
1131
|
+
return env_var
|
1108
1132
|
|
1109
1133
|
try:
|
1110
1134
|
pretty = repr(self.default_value)
|
@@ -1124,6 +1148,14 @@ class Variable(Doc[None]):
|
|
1124
1148
|
return ""
|
1125
1149
|
|
1126
1150
|
|
1151
|
+
@cache
|
1152
|
+
def _environ_lookup():
|
1153
|
+
"""
|
1154
|
+
A reverse lookup of os.environ. This is a cached function so that it is evaluated lazily.
|
1155
|
+
"""
|
1156
|
+
return {value: key for key, value in os.environ.items()}
|
1157
|
+
|
1158
|
+
|
1127
1159
|
class _PrettySignature(inspect.Signature):
|
1128
1160
|
"""
|
1129
1161
|
A subclass of `inspect.Signature` that pads __str__ over several lines
|
@@ -21,6 +21,7 @@ import warnings
|
|
21
21
|
|
22
22
|
import pdoc
|
23
23
|
|
24
|
+
from ._compat import ast_TypeAlias
|
24
25
|
from ._compat import ast_unparse
|
25
26
|
from ._compat import cache
|
26
27
|
|
@@ -115,7 +116,11 @@ def _walk_tree(
|
|
115
116
|
func_docstrings = {}
|
116
117
|
annotations = {}
|
117
118
|
for a, b in _pairwise_longest(_nodes(tree)):
|
118
|
-
if isinstance(a,
|
119
|
+
if isinstance(a, ast_TypeAlias):
|
120
|
+
name = a.name.id
|
121
|
+
elif (
|
122
|
+
isinstance(a, ast.AnnAssign) and isinstance(a.target, ast.Name) and a.simple
|
123
|
+
):
|
119
124
|
name = a.target.id
|
120
125
|
annotations[name] = unparse(a.annotation)
|
121
126
|
elif (
|
@@ -183,6 +188,8 @@ def sort_by_source(
|
|
183
188
|
name = a.target.id
|
184
189
|
elif isinstance(a, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
185
190
|
name = a.name
|
191
|
+
elif isinstance(a, ast_TypeAlias):
|
192
|
+
name = a.name.id
|
186
193
|
else:
|
187
194
|
continue
|
188
195
|
|
@@ -124,9 +124,9 @@ def safe_eval_type(
|
|
124
124
|
|
125
125
|
# Simple _eval_type has failed. We now execute all TYPE_CHECKING sections in the module and try again.
|
126
126
|
if module:
|
127
|
+
assert module.__dict__ is globalns
|
127
128
|
try:
|
128
|
-
|
129
|
-
eval(code, globalns, globalns)
|
129
|
+
_eval_type_checking_sections(module, set())
|
130
130
|
except Exception as e:
|
131
131
|
warnings.warn(
|
132
132
|
f"Failed to run TYPE_CHECKING code while parsing {t} type annotation for {fullname}: {e}"
|
@@ -148,7 +148,34 @@ def safe_eval_type(
|
|
148
148
|
f"Error parsing type annotation {t} for {fullname}. Import of {mod} failed: {err}"
|
149
149
|
)
|
150
150
|
return t
|
151
|
-
|
151
|
+
else:
|
152
|
+
globalns[mod] = val
|
153
|
+
return safe_eval_type(t, globalns, localns, module, fullname)
|
154
|
+
|
155
|
+
|
156
|
+
def _eval_type_checking_sections(module: types.ModuleType, seen: set) -> None:
|
157
|
+
"""
|
158
|
+
Evaluate all TYPE_CHECKING sections within a module.
|
159
|
+
|
160
|
+
The added complication here is that TYPE_CHECKING sections may import members from other modules' TYPE_CHECKING
|
161
|
+
sections. So we try to recursively execute those other modules' TYPE_CHECKING sections as well.
|
162
|
+
See https://github.com/mitmproxy/pdoc/issues/648 for a real world example.
|
163
|
+
"""
|
164
|
+
if module.__name__ in seen:
|
165
|
+
raise RecursionError(f"Recursion error when importing {module.__name__}.")
|
166
|
+
seen.add(module.__name__)
|
167
|
+
|
168
|
+
code = compile(type_checking_sections(module), "<string>", "exec")
|
169
|
+
while True:
|
170
|
+
try:
|
171
|
+
eval(code, module.__dict__, module.__dict__)
|
172
|
+
except ImportError as e:
|
173
|
+
if e.name is not None and (mod := sys.modules.get(e.name, None)):
|
174
|
+
_eval_type_checking_sections(mod, seen)
|
175
|
+
else:
|
176
|
+
raise
|
177
|
+
else:
|
178
|
+
break
|
152
179
|
|
153
180
|
|
154
181
|
def _eval_type(t, globalns, localns, recursive_guard=frozenset()):
|
@@ -393,7 +393,9 @@ def _rst_admonitions(contents: str, source_file: Path | None) -> str:
|
|
393
393
|
f"{indent(contents, ind)}\n"
|
394
394
|
f"{ind}</div>\n"
|
395
395
|
)
|
396
|
-
|
396
|
+
if type == "code-block":
|
397
|
+
return f"{ind}```{val}\n{contents}\n```\n"
|
398
|
+
if type == "versionadded":
|
397
399
|
text = f"New in version {val}"
|
398
400
|
elif type == "versionchanged":
|
399
401
|
text = f"Changed in version {val}"
|
@@ -409,7 +411,7 @@ def _rst_admonitions(contents: str, source_file: Path | None) -> str:
|
|
409
411
|
|
410
412
|
return text
|
411
413
|
|
412
|
-
admonition = "note|warning|danger|versionadded|versionchanged|deprecated|seealso|math|include"
|
414
|
+
admonition = "note|warning|danger|versionadded|versionchanged|deprecated|seealso|math|include|code-block"
|
413
415
|
return re.sub(
|
414
416
|
rf"""
|
415
417
|
^(?P<indent>[ ]*)\.\.[ ]+(?P<type>{admonition})::(?P<val>.*)
|
@@ -204,9 +204,7 @@ def mock_some_common_side_effects():
|
|
204
204
|
"os.startfile", new=_noop, create=True
|
205
205
|
), patch("sys.stdout", new=io.StringIO()), patch(
|
206
206
|
"sys.stderr", new=io.StringIO()
|
207
|
-
), patch(
|
208
|
-
"sys.stdin", new=io.StringIO()
|
209
|
-
):
|
207
|
+
), patch("sys.stdin", new=io.StringIO()):
|
210
208
|
yield
|
211
209
|
|
212
210
|
|
@@ -229,22 +227,71 @@ but we don't want to catch a user's KeyboardInterrupt.
|
|
229
227
|
"""
|
230
228
|
|
231
229
|
|
230
|
+
def iter_modules2(module: types.ModuleType) -> dict[str, pkgutil.ModuleInfo]:
|
231
|
+
"""
|
232
|
+
Returns all direct child modules of a given module.
|
233
|
+
This function is similar to `pkgutil.iter_modules`, but
|
234
|
+
|
235
|
+
1. Respects a package's `__all__` attribute if specified.
|
236
|
+
If `__all__` is defined, submodules not listed in `__all__` are excluded.
|
237
|
+
2. It will try to detect submodules that are not findable with iter_modules,
|
238
|
+
but are present in the module object.
|
239
|
+
"""
|
240
|
+
mod_all = getattr(module, "__all__", None)
|
241
|
+
|
242
|
+
submodules = {}
|
243
|
+
|
244
|
+
for submodule in pkgutil.iter_modules(
|
245
|
+
getattr(module, "__path__", []), f"{module.__name__}."
|
246
|
+
):
|
247
|
+
name = submodule.name.rpartition(".")[2]
|
248
|
+
if mod_all is None or name in mod_all:
|
249
|
+
submodules[name] = submodule
|
250
|
+
|
251
|
+
# 2023-12: PyO3 and pybind11 submodules are not detected by pkgutil
|
252
|
+
# This is a hacky workaround to register them.
|
253
|
+
members = dir(module) if mod_all is None else mod_all
|
254
|
+
for name in members:
|
255
|
+
if name in submodules or name == "__main__":
|
256
|
+
continue
|
257
|
+
member = getattr(module, name, None)
|
258
|
+
is_wild_child_module = (
|
259
|
+
isinstance(member, types.ModuleType)
|
260
|
+
# the name is either just "bar", but can also be "foo.bar",
|
261
|
+
# see https://github.com/PyO3/pyo3/issues/759#issuecomment-1811992321
|
262
|
+
and (
|
263
|
+
member.__name__ == f"{module.__name__}.{name}"
|
264
|
+
or (
|
265
|
+
member.__name__ == name
|
266
|
+
and sys.modules.get(member.__name__, None) is not member
|
267
|
+
)
|
268
|
+
)
|
269
|
+
)
|
270
|
+
if is_wild_child_module:
|
271
|
+
# fixup the module name so that the rest of pdoc does not break
|
272
|
+
assert member
|
273
|
+
member.__name__ = f"{module.__name__}.{name}"
|
274
|
+
sys.modules[f"{module.__name__}.{name}"] = member
|
275
|
+
submodules[name] = pkgutil.ModuleInfo(
|
276
|
+
None, # type: ignore
|
277
|
+
name=f"{module.__name__}.{name}",
|
278
|
+
ispkg=True,
|
279
|
+
)
|
280
|
+
|
281
|
+
submodules.pop("__main__", None) # https://github.com/mitmproxy/pdoc/issues/438
|
282
|
+
|
283
|
+
return submodules
|
284
|
+
|
285
|
+
|
232
286
|
def walk_packages2(
|
233
287
|
modules: Iterable[pkgutil.ModuleInfo],
|
234
288
|
) -> Iterator[pkgutil.ModuleInfo]:
|
235
289
|
"""
|
236
290
|
For a given list of modules, recursively yield their names and all their submodules' names.
|
237
291
|
|
238
|
-
This function is similar to `pkgutil.walk_packages`, but
|
239
|
-
If `__all__` is defined, submodules not listed in `__all__` are excluded.
|
292
|
+
This function is similar to `pkgutil.walk_packages`, but based on `iter_modules2`.
|
240
293
|
"""
|
241
|
-
|
242
|
-
# noinspection PyDefaultArgument
|
243
|
-
def seen(p, m={}): # pragma: no cover
|
244
|
-
if p in m:
|
245
|
-
return True
|
246
|
-
m[p] = True
|
247
|
-
|
294
|
+
# the original walk_packages implementation has a recursion check for path, but that does not seem to be needed?
|
248
295
|
for mod in modules:
|
249
296
|
yield mod
|
250
297
|
|
@@ -255,19 +302,8 @@ def walk_packages2(
|
|
255
302
|
warnings.warn(f"Error loading {mod.name}:\n{traceback.format_exc()}")
|
256
303
|
continue
|
257
304
|
|
258
|
-
|
259
|
-
|
260
|
-
path = [p for p in (getattr(module, "__path__", None) or []) if not seen(p)]
|
261
|
-
|
262
|
-
submodules = []
|
263
|
-
for submodule in pkgutil.iter_modules(path, f"{mod.name}."):
|
264
|
-
name = submodule.name.rpartition(".")[2]
|
265
|
-
if name == "__main__":
|
266
|
-
continue # https://github.com/mitmproxy/pdoc/issues/438
|
267
|
-
if mod_all is None or name in mod_all:
|
268
|
-
submodules.append(submodule)
|
269
|
-
|
270
|
-
yield from walk_packages2(submodules)
|
305
|
+
submodules = iter_modules2(module)
|
306
|
+
yield from walk_packages2(submodules.values())
|
271
307
|
|
272
308
|
|
273
309
|
def module_mtime(modulename: str) -> float | None:
|
@@ -5,6 +5,11 @@ and works without any third-party services in a privacy-preserving way. When a u
|
|
5
5
|
search box for the first time, pdoc will fetch the search index (`search.js`) and use that to
|
6
6
|
answer all upcoming queries.
|
7
7
|
|
8
|
+
##### Single-Page Documentation
|
9
|
+
|
10
|
+
If pdoc is documenting a single module only, search functionality will be disabled.
|
11
|
+
The browser's built-in search functionality will provide a better user experience in these cases.
|
12
|
+
|
8
13
|
##### Search Coverage
|
9
14
|
|
10
15
|
The search functionality covers all documented elements and their docstrings.
|
@@ -177,6 +177,7 @@ See https://pdoc.dev/docs/pdoc/render_helpers.html#DefaultMacroExtension for an
|
|
177
177
|
{% endif %}
|
178
178
|
{% enddefaultmacro %}
|
179
179
|
{% defaultmacro variable(var) -%}
|
180
|
+
{%- if var.is_type_alias_type %}<span class="def">type</span> {% endif -%}
|
180
181
|
<span class="name">{{ var.name }}</span>{{ annotation(var) }}{{ default_value(var) }}
|
181
182
|
{% enddefaultmacro %}
|
182
183
|
{% defaultmacro submodule(mod) -%}
|
@@ -44,7 +44,6 @@ pdoc = "pdoc.__main__:cli"
|
|
44
44
|
dev = [
|
45
45
|
"tox",
|
46
46
|
"ruff",
|
47
|
-
"black",
|
48
47
|
"mypy",
|
49
48
|
"types-pygments",
|
50
49
|
"pytest",
|
@@ -52,6 +51,7 @@ dev = [
|
|
52
51
|
"pytest-timeout",
|
53
52
|
"hypothesis",
|
54
53
|
"pygments >= 2.14.0",
|
54
|
+
"pdoc-pyo3-sample-library==1.0.11",
|
55
55
|
]
|
56
56
|
|
57
57
|
[build-system]
|
@@ -89,9 +89,6 @@ markers = [
|
|
89
89
|
"slow: marks tests as slow.",
|
90
90
|
]
|
91
91
|
|
92
|
-
[tool.black]
|
93
|
-
extend-exclude = "test/testdata/demo.py"
|
94
|
-
|
95
92
|
[[tool.mypy.overrides]]
|
96
93
|
module = "pytest.*"
|
97
94
|
ignore_missing_imports = true
|
@@ -101,8 +98,9 @@ module = "demopackage2"
|
|
101
98
|
ignore_missing_imports = true
|
102
99
|
|
103
100
|
[tool.ruff]
|
104
|
-
|
101
|
+
extend-exclude = ["test/testdata/demo.py"]
|
105
102
|
select = ["E", "F", "I"]
|
103
|
+
ignore = ["E501"]
|
106
104
|
|
107
105
|
[tool.ruff.isort]
|
108
106
|
force-single-line = true
|
@@ -11,6 +11,7 @@ from pdoc import extract
|
|
11
11
|
from pdoc.doc import Class
|
12
12
|
from pdoc.doc import Module
|
13
13
|
from pdoc.doc import Variable
|
14
|
+
from pdoc.doc import _environ_lookup
|
14
15
|
from pdoc.doc_types import empty
|
15
16
|
|
16
17
|
here = Path(__file__).parent
|
@@ -143,3 +144,34 @@ def test_raising_submodules():
|
|
143
144
|
assert m.submodules
|
144
145
|
finally:
|
145
146
|
f.write_bytes(b"# syntax error will be inserted by test here\n")
|
147
|
+
|
148
|
+
|
149
|
+
def test_default_value_masks_env_vars(monkeypatch):
|
150
|
+
monkeypatch.setenv("SUPER_SECRET_TOKEN", "correct horse battery staple")
|
151
|
+
monkeypatch.setenv("VERSION_NUMBER", "42.0.1")
|
152
|
+
_environ_lookup.cache_clear()
|
153
|
+
try:
|
154
|
+
v1 = Variable(
|
155
|
+
"module",
|
156
|
+
"var",
|
157
|
+
taken_from=("module", "var"),
|
158
|
+
docstring="",
|
159
|
+
annotation=empty,
|
160
|
+
default_value="correct horse battery staple",
|
161
|
+
)
|
162
|
+
with pytest.warns(
|
163
|
+
match=r"The default value of module.var matches the \$SUPER_SECRET_TOKEN environment variable."
|
164
|
+
):
|
165
|
+
assert v1.default_value_str == "$SUPER_SECRET_TOKEN"
|
166
|
+
|
167
|
+
v2 = Variable(
|
168
|
+
"module",
|
169
|
+
"version",
|
170
|
+
taken_from=("module", "version"),
|
171
|
+
docstring="",
|
172
|
+
annotation=empty,
|
173
|
+
default_value="42.0.1",
|
174
|
+
)
|
175
|
+
assert v2.default_value_str == "'42.0.1'"
|
176
|
+
finally:
|
177
|
+
_environ_lookup.cache_clear()
|
@@ -12,8 +12,9 @@ from pdoc.doc_types import safe_eval_type
|
|
12
12
|
"typestr", ["totally_unknown_module", "!!!!", "html.unknown_attr"]
|
13
13
|
)
|
14
14
|
def test_eval_fail(typestr):
|
15
|
+
a = types.ModuleType("a")
|
15
16
|
with pytest.warns(UserWarning, match="Error parsing type annotation"):
|
16
|
-
assert safe_eval_type(typestr,
|
17
|
+
assert safe_eval_type(typestr, a.__dict__, None, a, "a") == typestr
|
17
18
|
|
18
19
|
|
19
20
|
def test_eval_fail2(monkeypatch):
|
@@ -22,8 +23,9 @@ def test_eval_fail2(monkeypatch):
|
|
22
23
|
"get_source",
|
23
24
|
lambda _: "import typing\nif typing.TYPE_CHECKING:\n\traise RuntimeError()",
|
24
25
|
)
|
26
|
+
a = types.ModuleType("a")
|
25
27
|
with pytest.warns(UserWarning, match="Failed to run TYPE_CHECKING code"):
|
26
|
-
assert safe_eval_type("xyz",
|
28
|
+
assert safe_eval_type("xyz", a.__dict__, None, a, "a") == "xyz"
|
27
29
|
|
28
30
|
|
29
31
|
def test_eval_fail3(monkeypatch):
|
@@ -32,16 +34,24 @@ def test_eval_fail3(monkeypatch):
|
|
32
34
|
"get_source",
|
33
35
|
lambda _: "import typing\nif typing.TYPE_CHECKING:\n\tFooFn = typing.Callable[[],int]",
|
34
36
|
)
|
37
|
+
a = types.ModuleType("a")
|
38
|
+
a.__dict__["typing"] = typing
|
35
39
|
with pytest.warns(
|
36
40
|
UserWarning,
|
37
41
|
match="Error parsing type annotation .+ after evaluating TYPE_CHECKING blocks",
|
38
42
|
):
|
39
|
-
assert (
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
assert safe_eval_type("FooFn[int]", a.__dict__, None, a, "a") == "FooFn[int]"
|
44
|
+
|
45
|
+
|
46
|
+
def test_eval_fail_import_nonexistent(monkeypatch):
|
47
|
+
monkeypatch.setattr(
|
48
|
+
doc_ast,
|
49
|
+
"get_source",
|
50
|
+
lambda _: "import typing\nif typing.TYPE_CHECKING:\n\timport nonexistent_module",
|
51
|
+
)
|
52
|
+
a = types.ModuleType("a")
|
53
|
+
with pytest.warns(UserWarning, match="No module named 'nonexistent_module'"):
|
54
|
+
assert safe_eval_type("xyz", a.__dict__, None, a, "a") == "xyz"
|
45
55
|
|
46
56
|
|
47
57
|
def test_eval_union_types_on_old_python(monkeypatch):
|
@@ -53,3 +63,21 @@ def test_eval_union_types_on_old_python(monkeypatch):
|
|
53
63
|
):
|
54
64
|
# str never implements `|`, so we can use that to trigger the error on newer versions.
|
55
65
|
safe_eval_type('"foo" | "bar"', {}, None, None, "example")
|
66
|
+
|
67
|
+
|
68
|
+
def test_recurse(monkeypatch):
|
69
|
+
def get_source(mod):
|
70
|
+
if mod == a:
|
71
|
+
return "import typing\nif typing.TYPE_CHECKING:\n\tfrom b import Foo"
|
72
|
+
else:
|
73
|
+
return "import typing\nif typing.TYPE_CHECKING:\n\tfrom a import Foo"
|
74
|
+
|
75
|
+
a = types.ModuleType("a")
|
76
|
+
b = types.ModuleType("b")
|
77
|
+
|
78
|
+
monkeypatch.setattr(doc_ast, "get_source", get_source)
|
79
|
+
monkeypatch.setitem(sys.modules, "a", a)
|
80
|
+
monkeypatch.setitem(sys.modules, "b", b)
|
81
|
+
|
82
|
+
with pytest.warns(UserWarning, match="Recursion error when importing a"):
|
83
|
+
assert safe_eval_type("xyz", a.__dict__, None, a, "a") == "xyz"
|
@@ -60,6 +60,14 @@ def test_walk_specs():
|
|
60
60
|
"test.mod_with_main.__main__",
|
61
61
|
]
|
62
62
|
|
63
|
+
assert walk_specs(["pdoc_pyo3_sample_library"]) == [
|
64
|
+
"pdoc_pyo3_sample_library",
|
65
|
+
"pdoc_pyo3_sample_library.submodule",
|
66
|
+
"pdoc_pyo3_sample_library.submodule.subsubmodule",
|
67
|
+
"pdoc_pyo3_sample_library.explicit_submodule",
|
68
|
+
"pdoc_pyo3_sample_library.correct_name_submodule",
|
69
|
+
]
|
70
|
+
|
63
71
|
|
64
72
|
def test_parse_spec(monkeypatch):
|
65
73
|
p = sys.path
|
@@ -29,14 +29,14 @@ class Snapshot:
|
|
29
29
|
def __init__(
|
30
30
|
self,
|
31
31
|
id: str,
|
32
|
-
|
32
|
+
specs: list[str] | None = None,
|
33
33
|
render_options: dict | None = None,
|
34
34
|
with_output_directory: bool = False,
|
35
35
|
min_version: tuple[int, int] = (3, 7),
|
36
36
|
warnings: list[str] | None = None,
|
37
37
|
):
|
38
38
|
self.id = id
|
39
|
-
self.specs =
|
39
|
+
self.specs = specs or [f"{id}.py"]
|
40
40
|
self.render_options = render_options or {}
|
41
41
|
self.with_output_directory = with_output_directory
|
42
42
|
self.min_version = min_version
|
@@ -145,6 +145,7 @@ snapshots = [
|
|
145
145
|
min_version=(3, 12),
|
146
146
|
),
|
147
147
|
Snapshot("math_demo", render_options={"math": True}),
|
148
|
+
Snapshot("math_misc", render_options={"math": True}),
|
148
149
|
Snapshot("mermaid_demo", render_options={"mermaid": True}, min_version=(3, 9)),
|
149
150
|
Snapshot(
|
150
151
|
"render_options",
|
@@ -159,8 +160,9 @@ snapshots = [
|
|
159
160
|
},
|
160
161
|
with_output_directory=True,
|
161
162
|
),
|
163
|
+
Snapshot("pyo3_sample_library", specs=["pdoc_pyo3_sample_library"]),
|
162
164
|
Snapshot("top_level_reimports", ["top_level_reimports"]),
|
163
|
-
Snapshot("type_checking_imports"),
|
165
|
+
Snapshot("type_checking_imports", ["type_checking_imports.main"]),
|
164
166
|
Snapshot("type_stub", min_version=(3, 10)),
|
165
167
|
Snapshot(
|
166
168
|
"visibility",
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|