pdoc 14.0.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.
Files changed (74) hide show
  1. {pdoc-14.0.0 → pdoc-14.2.0}/CHANGELOG.md +32 -0
  2. {pdoc-14.0.0/pdoc.egg-info → pdoc-14.2.0}/PKG-INFO +3 -2
  3. {pdoc-14.0.0 → pdoc-14.2.0}/README.md +1 -1
  4. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/__init__.py +4 -3
  5. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/__main__.py +1 -1
  6. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/_compat.py +21 -67
  7. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/doc.py +45 -13
  8. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/doc_ast.py +8 -1
  9. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/doc_pyi.py +3 -7
  10. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/doc_types.py +30 -3
  11. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/docstrings.py +4 -2
  12. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/extract.py +61 -25
  13. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/render_helpers.py +40 -15
  14. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/search.py +5 -0
  15. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/content.css +1 -1
  16. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/default/module.html.jinja2 +1 -0
  17. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/layout.css +2 -1
  18. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/web.py +8 -1
  19. {pdoc-14.0.0 → pdoc-14.2.0/pdoc.egg-info}/PKG-INFO +3 -2
  20. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc.egg-info/requires.txt +1 -1
  21. {pdoc-14.0.0 → pdoc-14.2.0}/pyproject.toml +4 -5
  22. {pdoc-14.0.0 → pdoc-14.2.0}/test/test_doc.py +32 -0
  23. {pdoc-14.0.0 → pdoc-14.2.0}/test/test_doc_types.py +36 -8
  24. {pdoc-14.0.0 → pdoc-14.2.0}/test/test_extract.py +9 -0
  25. {pdoc-14.0.0 → pdoc-14.2.0}/test/test_snapshot.py +5 -3
  26. {pdoc-14.0.0 → pdoc-14.2.0}/test/test_web.py +6 -0
  27. {pdoc-14.0.0 → pdoc-14.2.0}/LICENSE +0 -0
  28. {pdoc-14.0.0 → pdoc-14.2.0}/MANIFEST.in +0 -0
  29. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/markdown2/LICENSE +0 -0
  30. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/markdown2/README.md +0 -0
  31. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/markdown2/__init__.py +0 -0
  32. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/py.typed +0 -0
  33. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/render.py +0 -0
  34. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/README.md +0 -0
  35. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/build-search-index.js +0 -0
  36. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/custom.css +0 -0
  37. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/default/error.html.jinja2 +0 -0
  38. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/default/frame.html.jinja2 +0 -0
  39. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/default/index.html.jinja2 +0 -0
  40. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/deprecated/README.md +0 -0
  41. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/deprecated/bootstrap-reboot.min.css +0 -0
  42. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/deprecated/box-arrow-in-left.svg +0 -0
  43. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/deprecated/elasticlunr.min.js +0 -0
  44. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/deprecated/favicon.svg +0 -0
  45. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/deprecated/navtoggle.svg +0 -0
  46. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/deprecated/pdoc-logo.svg +0 -0
  47. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/deprecated/resources/favicon.svg +0 -0
  48. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/livereload.html.jinja2 +0 -0
  49. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/math.html.jinja2 +0 -0
  50. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/mermaid.html.jinja2 +0 -0
  51. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/resources/bootstrap-reboot.min.css +0 -0
  52. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/resources/box-arrow-in-left.svg +0 -0
  53. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/resources/elasticlunr.min.js +0 -0
  54. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/resources/exclamation-triangle-fill.svg +0 -0
  55. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/resources/info-circle-fill.svg +0 -0
  56. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/resources/lightning-fill.svg +0 -0
  57. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/resources/navtoggle.svg +0 -0
  58. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/resources/pdoc-logo.svg +0 -0
  59. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/search.html.jinja2 +0 -0
  60. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/search.js.jinja2 +0 -0
  61. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/syntax-highlighting.css +0 -0
  62. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc/templates/theme.css +0 -0
  63. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc.egg-info/SOURCES.txt +0 -0
  64. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc.egg-info/dependency_links.txt +0 -0
  65. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc.egg-info/entry_points.txt +0 -0
  66. {pdoc-14.0.0 → pdoc-14.2.0}/pdoc.egg-info/top_level.txt +0 -0
  67. {pdoc-14.0.0 → pdoc-14.2.0}/setup.cfg +0 -0
  68. {pdoc-14.0.0 → pdoc-14.2.0}/test/test_doc_ast.py +0 -0
  69. {pdoc-14.0.0 → pdoc-14.2.0}/test/test_doc_pyi.py +0 -0
  70. {pdoc-14.0.0 → pdoc-14.2.0}/test/test_docstrings.py +0 -0
  71. {pdoc-14.0.0 → pdoc-14.2.0}/test/test_main.py +0 -0
  72. {pdoc-14.0.0 → pdoc-14.2.0}/test/test_render_helpers.py +0 -0
  73. {pdoc-14.0.0 → pdoc-14.2.0}/test/test_search.py +0 -0
  74. {pdoc-14.0.0 → pdoc-14.2.0}/test/test_smoke.py +0 -0
@@ -4,6 +4,38 @@
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)
23
+
24
+ ## 2023-09-10: pdoc 14.1.0
25
+
26
+ - Add compatibility with Python 3.12
27
+ ([#620](https://github.com/mitmproxy/pdoc/pull/620), @mhils)
28
+ - Add support for relative links. Instead of explicitly referring to `mypackage.helpers.foo`,
29
+ one can now also refer to `.helpers.foo` within the `mypackage` module, or `..helpers.foo` in a submodule.
30
+ ([#544](https://github.com/mitmproxy/pdoc/pull/544), @Crozzers)
31
+ - Function signatures will now display "Foo" instead "demo.Foo" if the function is in the same module.
32
+ ([#544](https://github.com/mitmproxy/pdoc/pull/544), @mhils)
33
+ - pdoc now also picks up docstrings from `.pyi` stub files.
34
+ ([#619](https://github.com/mitmproxy/pdoc/pull/619), @mhils)
35
+ - Fix horizontal scroll navigation z-index issue.
36
+ ([#616](https://github.com/mitmproxy/pdoc/pull/616), @Domi04151309)
37
+ - Be more strict about parsing URLs in pdoc's web server.
38
+ ([#617](https://github.com/mitmproxy/pdoc/pull/617), @mhils)
7
39
 
8
40
  ## 2023-06-19: pdoc 14.0.0
9
41
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pdoc
3
- Version: 14.0.0
3
+ Version: 14.2.0
4
4
  Summary: API Documentation for Python Projects
5
5
  Author-email: Maximilian Hils <pdoc@maximilianhils.com>
6
6
  License: Unlicense
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.8
20
20
  Classifier: Programming Language :: Python :: 3.9
21
21
  Classifier: Programming Language :: Python :: 3.10
22
22
  Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
23
24
  Classifier: Typing :: Typed
24
25
  Requires-Python: >=3.8
25
26
  Description-Content-Type: text/markdown
@@ -30,7 +31,7 @@ License-File: LICENSE
30
31
  <a href="https://pdoc.dev/"><img alt="pdoc" src="https://pdoc.dev/logo.svg" width="200" height="100" /></a>
31
32
  <br><br>
32
33
  <a href="https://pdoc.dev/docs/pdoc.html"><img height="20" alt="pdoc documentation" src="https://shields.mitmproxy.org/badge/docs-pdoc.dev-brightgreen.svg"></a>
33
- <img height="20" alt="CI Status" src="https://shields.mitmproxy.org/github/workflow/status/mitmproxy/pdoc/CI?label=CI&logo=github">
34
+ <img height="20" alt="CI Status" src="https://shields.mitmproxy.org/github/actions/workflow/status/mitmproxy/pdoc/main.yml?label=CI&logo=github">
34
35
  <img height="20" alt="Code Coverage" src="https://shields.mitmproxy.org/badge/coverage-100%25-brightgreen">
35
36
  <a href="https://autofix.ci"><img height="20" alt="autofix.ci: yes" src="https://shields.mitmproxy.org/badge/autofix.ci-yes-success?logo=data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjZmZmIiB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCB0cmFuc2Zvcm09InNjYWxlKDAuMDYxLC0wLjA2MSkgdHJhbnNsYXRlKC0yNTAsLTE3NTApIiBkPSJNMTMyNSAtMzQwcS0xMTUgMCAtMTY0LjUgMzIuNXQtNDkuNSAxMTQuNXEwIDMyIDUgNzAuNXQxMC41IDcyLjV0NS41IDU0djIyMHEtMzQgLTkgLTY5LjUgLTE0dC03MS41IC01cS0xMzYgMCAtMjUxLjUgNjJ0LTE5MSAxNjl0LTkyLjUgMjQxcS05MCAxMjAgLTkwIDI2NnEwIDEwOCA0OC41IDIwMC41dDEzMiAxNTUuNXQxODguNSA4MXExNSA5OSAxMDAuNSAxODAuNXQyMTcgMTMwLjV0MjgyLjUgNDlxMTM2IDAgMjU2LjUgLTQ2IHQyMDkgLTEyNy41dDEyOC41IC0xODkuNXExNDkgLTgyIDIyNyAtMjEzLjV0NzggLTI5OS41cTAgLTEzNiAtNTggLTI0NnQtMTY1LjUgLTE4NC41dC0yNTYuNSAtMTAzLjVsLTI0MyAtMzAwdi01MnEwIC0yNyAzLjUgLTU2LjV0Ni41IC01Ny41dDMgLTUycTAgLTg1IC00MS41IC0xMTguNXQtMTU3LjUgLTMzLjV6TTEzMjUgLTI2MHE3NyAwIDk4IDE0LjV0MjEgNTcuNXEwIDI5IC0zIDY4dC02LjUgNzN0LTMuNSA0OHY2NGwyMDcgMjQ5IHEtMzEgMCAtNjAgNS41dC01NCAxMi41bC0xMDQgLTEyM3EtMSAzNCAtMiA2My41dC0xIDU0LjVxMCA2OSA5IDEyM2wzMSAyMDBsLTExNSAtMjhsLTQ2IC0yNzFsLTIwNSAyMjZxLTE5IC0xNSAtNDMgLTI4LjV0LTU1IC0yNi41bDIxOSAtMjQydi0yNzZxMCAtMjAgLTUuNSAtNjB0LTEwLjUgLTc5dC01IC01OHEwIC00MCAzMCAtNTMuNXQxMDQgLTEzLjV6TTEyNjIgNjE2cS0xMTkgMCAtMjI5LjUgMzQuNXQtMTkzLjUgOTYuNWw0OCA2NCBxNzMgLTU1IDE3MC41IC04NXQyMDQuNSAtMzBxMTM3IDAgMjQ5IDQ1LjV0MTc5IDEyMXQ2NyAxNjUuNWg4MHEwIC0xMTQgLTc3LjUgLTIwNy41dC0yMDggLTE0OXQtMjg5LjUgLTU1LjV6TTgwMyA1OTVxODAgMCAxNDkgMjkuNXQxMDggNzIuNWwyMjEgLTY3bDMwOSA4NnE0NyAtMzIgMTA0LjUgLTUwdDExNy41IC0xOHE5MSAwIDE2NSAzOHQxMTguNSAxMDMuNXQ0NC41IDE0Ni41cTAgNzYgLTM0LjUgMTQ5dC05NS41IDEzNHQtMTQzIDk5IHEtMzcgMTA3IC0xMTUuNSAxODMuNXQtMTg2IDExNy41dC0yMzAuNSA0MXEtMTAzIDAgLTE5Ny41IC0yNnQtMTY5IC03Mi41dC0xMTcuNSAtMTA4dC00MyAtMTMxLjVxMCAtMzQgMTQuNSAtNjIuNXQ0MC41IC01MC41bC01NSAtNTlxLTM0IDI5IC01NCA2NS41dC0yNSA4MS41cS04MSAtMTggLTE0NSAtNzB0LTEwMSAtMTI1LjV0LTM3IC0xNTguNXEwIC0xMDIgNDguNSAtMTgwLjV0MTI5LjUgLTEyM3QxNzkgLTQ0LjV6Ii8+PC9zdmc+"></a>
36
37
  <a href="https://pypi.python.org/pypi/pdoc"><img height="20" alt="PyPI Version" src="https://shields.mitmproxy.org/pypi/v/pdoc.svg"></a>
@@ -2,7 +2,7 @@
2
2
  <a href="https://pdoc.dev/"><img alt="pdoc" src="https://pdoc.dev/logo.svg" width="200" height="100" /></a>
3
3
  <br><br>
4
4
  <a href="https://pdoc.dev/docs/pdoc.html"><img height="20" alt="pdoc documentation" src="https://shields.mitmproxy.org/badge/docs-pdoc.dev-brightgreen.svg"></a>
5
- <img height="20" alt="CI Status" src="https://shields.mitmproxy.org/github/workflow/status/mitmproxy/pdoc/CI?label=CI&logo=github">
5
+ <img height="20" alt="CI Status" src="https://shields.mitmproxy.org/github/actions/workflow/status/mitmproxy/pdoc/main.yml?label=CI&logo=github">
6
6
  <img height="20" alt="Code Coverage" src="https://shields.mitmproxy.org/badge/coverage-100%25-brightgreen">
7
7
  <a href="https://autofix.ci"><img height="20" alt="autofix.ci: yes" src="https://shields.mitmproxy.org/badge/autofix.ci-yes-success?logo=data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjZmZmIiB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCB0cmFuc2Zvcm09InNjYWxlKDAuMDYxLC0wLjA2MSkgdHJhbnNsYXRlKC0yNTAsLTE3NTApIiBkPSJNMTMyNSAtMzQwcS0xMTUgMCAtMTY0LjUgMzIuNXQtNDkuNSAxMTQuNXEwIDMyIDUgNzAuNXQxMC41IDcyLjV0NS41IDU0djIyMHEtMzQgLTkgLTY5LjUgLTE0dC03MS41IC01cS0xMzYgMCAtMjUxLjUgNjJ0LTE5MSAxNjl0LTkyLjUgMjQxcS05MCAxMjAgLTkwIDI2NnEwIDEwOCA0OC41IDIwMC41dDEzMiAxNTUuNXQxODguNSA4MXExNSA5OSAxMDAuNSAxODAuNXQyMTcgMTMwLjV0MjgyLjUgNDlxMTM2IDAgMjU2LjUgLTQ2IHQyMDkgLTEyNy41dDEyOC41IC0xODkuNXExNDkgLTgyIDIyNyAtMjEzLjV0NzggLTI5OS41cTAgLTEzNiAtNTggLTI0NnQtMTY1LjUgLTE4NC41dC0yNTYuNSAtMTAzLjVsLTI0MyAtMzAwdi01MnEwIC0yNyAzLjUgLTU2LjV0Ni41IC01Ny41dDMgLTUycTAgLTg1IC00MS41IC0xMTguNXQtMTU3LjUgLTMzLjV6TTEzMjUgLTI2MHE3NyAwIDk4IDE0LjV0MjEgNTcuNXEwIDI5IC0zIDY4dC02LjUgNzN0LTMuNSA0OHY2NGwyMDcgMjQ5IHEtMzEgMCAtNjAgNS41dC01NCAxMi41bC0xMDQgLTEyM3EtMSAzNCAtMiA2My41dC0xIDU0LjVxMCA2OSA5IDEyM2wzMSAyMDBsLTExNSAtMjhsLTQ2IC0yNzFsLTIwNSAyMjZxLTE5IC0xNSAtNDMgLTI4LjV0LTU1IC0yNi41bDIxOSAtMjQydi0yNzZxMCAtMjAgLTUuNSAtNjB0LTEwLjUgLTc5dC01IC01OHEwIC00MCAzMCAtNTMuNXQxMDQgLTEzLjV6TTEyNjIgNjE2cS0xMTkgMCAtMjI5LjUgMzQuNXQtMTkzLjUgOTYuNWw0OCA2NCBxNzMgLTU1IDE3MC41IC04NXQyMDQuNSAtMzBxMTM3IDAgMjQ5IDQ1LjV0MTc5IDEyMXQ2NyAxNjUuNWg4MHEwIC0xMTQgLTc3LjUgLTIwNy41dC0yMDggLTE0OXQtMjg5LjUgLTU1LjV6TTgwMyA1OTVxODAgMCAxNDkgMjkuNXQxMDggNzIuNWwyMjEgLTY3bDMwOSA4NnE0NyAtMzIgMTA0LjUgLTUwdDExNy41IC0xOHE5MSAwIDE2NSAzOHQxMTguNSAxMDMuNXQ0NC41IDE0Ni41cTAgNzYgLTM0LjUgMTQ5dC05NS41IDEzNHQtMTQzIDk5IHEtMzcgMTA3IC0xMTUuNSAxODMuNXQtMTg2IDExNy41dC0yMzAuNSA0MXEtMTAzIDAgLTE5Ny41IC0yNnQtMTY5IC03Mi41dC0xMTcuNSAtMTA4dC00MyAtMTMxLjVxMCAtMzQgMTQuNSAtNjIuNXQ0MC41IC01MC41bC01NSAtNTlxLTM0IDI5IC01NCA2NS41dC0yNSA4MS41cS04MSAtMTggLTE0NSAtNzB0LTEwMSAtMTI1LjV0LTM3IC0xNTguNXEwIC0xMDIgNDguNSAtMTgwLjV0MTI5LjUgLTEyM3QxNzkgLTQ0LjV6Ii8+PC9zdmc+"></a>
8
8
  <a href="https://pypi.python.org/pypi/pdoc"><img height="20" alt="PyPI Version" src="https://shields.mitmproxy.org/pypi/v/pdoc.svg"></a>
@@ -180,13 +180,14 @@ ways.
180
180
  - If `__all__` is defined in the module, then all identifiers in that list will be considered public.
181
181
  No other identifiers will be considered public.
182
182
  - If `__all__` is not defined, then pdoc will consider all members public that
183
- 1. do not start with an underscore
183
+ 1. do not start with an underscore,
184
+ 2. don't have `@private` in their docstring,
184
185
  2. and are defined in the current module (i.e. they are not imported).
185
186
 
186
187
  In general, we recommend keeping these conventions:
187
188
 
188
189
  - If you want to document a private member, consider making it public.
189
- - If you want to hide a public member, consider making it private.
190
+ - If you want to hide a public member, consider making it private or add `@private` to their docstring,
190
191
  - If you want to document a special `__dunder__` method, the recommended way to do so is
191
192
  to not document the dunder method specifically, but to add some usage examples in the class documentation.
192
193
 
@@ -461,7 +462,7 @@ You can find an example in [`examples/library-usage`](https://github.com/mitmpro
461
462
  from __future__ import annotations
462
463
 
463
464
  __docformat__ = "markdown" # explicitly disable rST processing in the examples above.
464
- __version__ = "14.0.0" # this is read from setup.py
465
+ __version__ = "14.2.0" # this is read from setup.py
465
466
 
466
467
  from pathlib import Path
467
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
@@ -43,65 +61,6 @@ else: # pragma: no cover
43
61
  x = x[len(prefix):]
44
62
  return x
45
63
 
46
- if sys.version_info >= (3, 8):
47
- from functools import cached_property
48
- else: # pragma: no cover
49
- from threading import RLock
50
-
51
- # https://github.com/python/cpython/blob/863eb7170b3017399fb2b786a1e3feb6457e54c2/Lib/functools.py#L930-L980
52
- # ✂ start ✂
53
- _NOT_FOUND = object()
54
-
55
- class cached_property: # type: ignore
56
- def __init__(self, func):
57
- self.func = func
58
- self.attrname = None
59
- self.__doc__ = func.__doc__
60
- self.lock = RLock()
61
-
62
- def __set_name__(self, owner, name):
63
- if self.attrname is None:
64
- self.attrname = name
65
- elif name != self.attrname:
66
- raise TypeError(
67
- "Cannot assign the same cached_property to two different names "
68
- f"({self.attrname!r} and {name!r})."
69
- )
70
-
71
- def __get__(self, instance, owner=None):
72
- if instance is None:
73
- return self
74
- if self.attrname is None:
75
- raise TypeError(
76
- "Cannot use cached_property instance without calling __set_name__ on it.")
77
- try:
78
- cache = instance.__dict__
79
- except AttributeError: # not all objects have __dict__ (e.g. class defines slots)
80
- msg = (
81
- f"No '__dict__' attribute on {type(instance).__name__!r} "
82
- f"instance to cache {self.attrname!r} property."
83
- )
84
- raise TypeError(msg) from None
85
- val = cache.get(self.attrname, _NOT_FOUND)
86
- if val is _NOT_FOUND:
87
- with self.lock:
88
- # check if another thread filled cache while we awaited lock
89
- val = cache.get(self.attrname, _NOT_FOUND)
90
- if val is _NOT_FOUND:
91
- val = self.func(instance)
92
- try:
93
- cache[self.attrname] = val
94
- except TypeError:
95
- msg = (
96
- f"The '__dict__' attribute on {type(instance).__name__!r} instance "
97
- f"does not support item assignment for caching {self.attrname!r} property."
98
- )
99
- raise TypeError(msg) from None
100
- return val
101
-
102
- __class_getitem__ = classmethod(GenericAlias)
103
- # ✂ end ✂
104
-
105
64
 
106
65
  if (3, 9) <= sys.version_info < (3, 9, 8) or (3, 10) <= sys.version_info < (3, 10, 1): # pragma: no cover
107
66
  import inspect
@@ -117,12 +76,6 @@ if (3, 9) <= sys.version_info < (3, 9, 8) or (3, 10) <= sys.version_info < (3, 1
117
76
  else:
118
77
  from inspect import formatannotation
119
78
 
120
- if sys.version_info >= (3, 8):
121
- from functools import singledispatchmethod
122
- else: # pragma: no cover
123
- class singledispatchmethod:
124
- pass # pragma: no cover
125
-
126
79
  if sys.version_info >= (3, 9):
127
80
  from argparse import BooleanOptionalAction
128
81
  else: # pragma: no cover
@@ -173,11 +126,12 @@ else: # pragma: no cover
173
126
  __all__ = [
174
127
  "cache",
175
128
  "ast_unparse",
129
+ "ast_TypeAlias",
130
+ "TypeAliasType",
131
+ "TypeAlias",
176
132
  "GenericAlias",
177
133
  "UnionType",
178
134
  "removesuffix",
179
- "cached_property",
180
135
  "formatannotation",
181
- "singledispatchmethod",
182
136
  "BooleanOptionalAction",
183
137
  ]
@@ -22,11 +22,12 @@ from abc import abstractmethod
22
22
  from collections.abc import Callable
23
23
  import dataclasses
24
24
  import enum
25
+ from functools import cached_property
26
+ from functools import singledispatchmethod
25
27
  from functools import wraps
26
28
  import inspect
27
29
  import os
28
30
  from pathlib import Path
29
- import pkgutil
30
31
  import re
31
32
  import sys
32
33
  import textwrap
@@ -43,17 +44,16 @@ import warnings
43
44
  from pdoc import doc_ast
44
45
  from pdoc import doc_pyi
45
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
46
51
  from pdoc.doc_types import GenericAlias
47
52
  from pdoc.doc_types import NonUserDefinedCallables
48
53
  from pdoc.doc_types import empty
49
54
  from pdoc.doc_types import resolve_annotations
50
55
  from pdoc.doc_types import safe_eval_type
51
56
 
52
- from ._compat import cache
53
- from ._compat import cached_property
54
- from ._compat import formatannotation
55
- from ._compat import singledispatchmethod
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 pkgutil.iter_modules(self.obj.__path__, f"{self.fullname}."):
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(p.annotation, globalns, localns, mod, self.fullname) # type: ignore
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, ast.AnnAssign) and isinstance(a.target, ast.Name) and a.simple:
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
 
@@ -70,16 +70,12 @@ def _patch_doc(target_doc: doc.Doc, stub_mod: doc.Module) -> None:
70
70
  if isinstance(target_doc, doc.Function) and isinstance(stub_doc, doc.Function):
71
71
  target_doc.signature = stub_doc.signature
72
72
  target_doc.funcdef = stub_doc.funcdef
73
+ target_doc.docstring = stub_doc.docstring or target_doc.docstring
73
74
  elif isinstance(target_doc, doc.Variable) and isinstance(stub_doc, doc.Variable):
74
75
  target_doc.annotation = stub_doc.annotation
76
+ target_doc.docstring = stub_doc.docstring or target_doc.docstring
75
77
  elif isinstance(target_doc, doc.Namespace) and isinstance(stub_doc, doc.Namespace):
76
- # pdoc currently does not include variables without docstring in .members (not ideal),
77
- # so the regular patching won't work. We manually copy over type annotations instead.
78
- for k, v in stub_doc._var_annotations.items():
79
- var = target_doc.members.get(k, None)
80
- if isinstance(var, doc.Variable):
81
- var.annotation = v
82
-
78
+ target_doc.docstring = stub_doc.docstring or target_doc.docstring
83
79
  for m in target_doc.members.values():
84
80
  _patch_doc(m, stub_mod)
85
81
  else:
@@ -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
- code = compile(type_checking_sections(module), "<string>", "exec")
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
- return safe_eval_type(t, {mod: val, **globalns}, localns, module, fullname)
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
- elif type == "versionadded":
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 respects a package's `__all__` attribute if specified.
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
- mod_all = getattr(module, "__all__", None)
259
- # don't traverse path items we've seen before
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:
@@ -43,7 +43,7 @@ formatter = pygments.formatters.HtmlFormatter(
43
43
  anchorlinenos=True,
44
44
  )
45
45
  """
46
- The pygments formatter used for pdoc.render_helpers.highlight.
46
+ The pygments formatter used for pdoc.render_helpers.highlight.
47
47
  Overwrite this to configure pygments highlighting of code blocks.
48
48
 
49
49
  The usage of the `.codehilite` CSS selector in custom templates is deprecated since pdoc 10, use `.pdoc-code` instead.
@@ -51,7 +51,7 @@ The usage of the `.codehilite` CSS selector in custom templates is deprecated si
51
51
 
52
52
  signature_formatter = pygments.formatters.HtmlFormatter(nowrap=True)
53
53
  """
54
- The pygments formatter used for pdoc.render_helpers.format_signature.
54
+ The pygments formatter used for pdoc.render_helpers.format_signature.
55
55
  Overwrite this to configure pygments highlighting of signatures.
56
56
  """
57
57
 
@@ -287,13 +287,30 @@ def linkify(context: Context, code: str, namespace: str = "") -> str:
287
287
  '</span><span class="o">.</span><span class="n">', "."
288
288
  )
289
289
  identifier = removesuffix(plain_text, "()")
290
-
291
- # Check if this is a local reference within this module?
292
290
  mod: pdoc.doc.Module = context["module"]
293
- for qualname in qualname_candidates(identifier, namespace):
294
- doc = mod.get(qualname)
295
- if doc and context["is_public"](doc).strip():
296
- return f'<a href="#{qualname}">{plain_text}</a>'
291
+
292
+ # Check if this is a relative reference?
293
+ if identifier.startswith("."):
294
+ taken_from_mod = mod
295
+ if namespace and (ns := mod.get(namespace)):
296
+ # Imported from somewhere else, so the relative reference should be from the original module.
297
+ taken_from_mod = context["all_modules"].get(ns.taken_from[0], mod)
298
+ if taken_from_mod.is_package:
299
+ # If we are in __init__.py, we want `.foo` to refer to a child module.
300
+ parent_module = taken_from_mod.modulename
301
+ else:
302
+ # If we are in a leaf module, we want `.foo` to refer to the adjacent module.
303
+ parent_module = taken_from_mod.modulename.rpartition(".")[0]
304
+ while identifier.startswith(".."):
305
+ identifier = identifier[1:]
306
+ parent_module = parent_module.rpartition(".")[0]
307
+ identifier = parent_module + identifier
308
+ else:
309
+ # Check if this is a local reference within this module?
310
+ for qualname in qualname_candidates(identifier, namespace):
311
+ doc = mod.get(qualname)
312
+ if doc and context["is_public"](doc).strip():
313
+ return f'<a href="#{qualname}">{plain_text}</a>'
297
314
 
298
315
  module = ""
299
316
  qualname = ""
@@ -309,9 +326,9 @@ def linkify(context: Context, code: str, namespace: str = "") -> str:
309
326
  and context["is_public"](doc).strip()
310
327
  ):
311
328
  if plain_text.endswith("()"):
312
- plain_text = f"{doc.fullname}()"
329
+ plain_text = f"{doc.qualname}()"
313
330
  else:
314
- plain_text = doc.fullname
331
+ plain_text = doc.qualname
315
332
  return f'<a href="#{qualname}">{plain_text}</a>'
316
333
  except ValueError:
317
334
  # possible_sources did not find a parent module.
@@ -326,8 +343,13 @@ def linkify(context: Context, code: str, namespace: str = "") -> str:
326
343
  doc is not None and context["is_public"](doc).strip()
327
344
  )
328
345
  if target_exists_and_public:
346
+ assert doc is not None # mypy
329
347
  if qualname:
330
348
  qualname = f"#{qualname}"
349
+ if plain_text.endswith("()"):
350
+ plain_text = f"{doc.fullname}()"
351
+ else:
352
+ plain_text = doc.fullname
331
353
  return f'<a href="{relative_link(context["module"].modulename, module)}{qualname}">{plain_text}</a>'
332
354
  else:
333
355
  return text
@@ -337,11 +359,14 @@ def linkify(context: Context, code: str, namespace: str = "") -> str:
337
359
  r"""
338
360
  # Part 1: foo.bar or foo.bar() (without backticks)
339
361
  (?<![/=?#&]) # heuristic: not part of a URL
340
- \b
341
-
342
- # First part of the identifier (e.g. "foo")
343
- (?!\d)[a-zA-Z0-9_]+
344
- # Rest of the identifier (e.g. ".bar")
362
+ # First part of the identifier (e.g. "foo") - this is optional for relative references.
363
+ (?:
364
+ \b
365
+ (?!\d)[a-zA-Z0-9_]+
366
+ |
367
+ \.* # We may also start with multiple dots.
368
+ )
369
+ # Rest of the identifier (e.g. ".bar" or "..bar")
345
370
  (?:
346
371
  # A single dot or a dot surrounded with pygments highlighting.
347
372
  (?:\.|</span><span\ class="o">\.</span><span\ class="n">)
@@ -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.
@@ -128,7 +128,7 @@ This makes sure that the pdoc styling doesn't leak to the rest of the page when
128
128
  padding: .2em .4em;
129
129
  margin: 0;
130
130
  font-size: 85%;
131
- background-color: var(--code);
131
+ background-color: var(--accent);
132
132
  border-radius: 6px;
133
133
  }
134
134
 
@@ -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) -%}
@@ -124,7 +124,8 @@ nav.pdoc {
124
124
  padding: 0 0 0 var(--pad);
125
125
  overflow-wrap: anywhere;
126
126
  scrollbar-width: thin; /* Scrollbar width on Firefox */
127
- scrollbar-color: var(--accent2) transparent /* Scrollbar color on Firefox */
127
+ scrollbar-color: var(--accent2) transparent; /* Scrollbar color on Firefox */
128
+ z-index: 1
128
129
  }
129
130
 
130
131
  nav.pdoc::-webkit-scrollbar {
@@ -40,7 +40,7 @@ class DocHandler(http.server.BaseHTTPRequestHandler):
40
40
  except ConnectionError: # pragma: no cover
41
41
  pass
42
42
 
43
- def handle_request(self) -> str | None:
43
+ def handle_request(self) -> str:
44
44
  """Actually handle a request. Called by `do_HEAD` and `do_GET`."""
45
45
  path = self.path.split("?", 1)[0]
46
46
 
@@ -51,6 +51,13 @@ class DocHandler(http.server.BaseHTTPRequestHandler):
51
51
  self.send_header("content-type", "application/javascript")
52
52
  self.end_headers()
53
53
  return self.server.render_search_index()
54
+ elif "." in removesuffix(path, ".html"):
55
+ # See https://github.com/mitmproxy/pdoc/issues/615: All module separators should be normalized to "/".
56
+ # We could redirect here, but that would create the impression of a working link, which will fall apart
57
+ # when pdoc prerenders to static HTML. So we rather fail early.
58
+ self.send_response(404)
59
+ self.end_headers()
60
+ return "Not Found: Please normalize all module separators to '/'."
54
61
  else:
55
62
  module_name = removesuffix(path.lstrip("/"), ".html").replace("/", ".")
56
63
  if module_name not in self.server.all_modules:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pdoc
3
- Version: 14.0.0
3
+ Version: 14.2.0
4
4
  Summary: API Documentation for Python Projects
5
5
  Author-email: Maximilian Hils <pdoc@maximilianhils.com>
6
6
  License: Unlicense
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.8
20
20
  Classifier: Programming Language :: Python :: 3.9
21
21
  Classifier: Programming Language :: Python :: 3.10
22
22
  Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
23
24
  Classifier: Typing :: Typed
24
25
  Requires-Python: >=3.8
25
26
  Description-Content-Type: text/markdown
@@ -30,7 +31,7 @@ License-File: LICENSE
30
31
  <a href="https://pdoc.dev/"><img alt="pdoc" src="https://pdoc.dev/logo.svg" width="200" height="100" /></a>
31
32
  <br><br>
32
33
  <a href="https://pdoc.dev/docs/pdoc.html"><img height="20" alt="pdoc documentation" src="https://shields.mitmproxy.org/badge/docs-pdoc.dev-brightgreen.svg"></a>
33
- <img height="20" alt="CI Status" src="https://shields.mitmproxy.org/github/workflow/status/mitmproxy/pdoc/CI?label=CI&logo=github">
34
+ <img height="20" alt="CI Status" src="https://shields.mitmproxy.org/github/actions/workflow/status/mitmproxy/pdoc/main.yml?label=CI&logo=github">
34
35
  <img height="20" alt="Code Coverage" src="https://shields.mitmproxy.org/badge/coverage-100%25-brightgreen">
35
36
  <a href="https://autofix.ci"><img height="20" alt="autofix.ci: yes" src="https://shields.mitmproxy.org/badge/autofix.ci-yes-success?logo=data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjZmZmIiB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCB0cmFuc2Zvcm09InNjYWxlKDAuMDYxLC0wLjA2MSkgdHJhbnNsYXRlKC0yNTAsLTE3NTApIiBkPSJNMTMyNSAtMzQwcS0xMTUgMCAtMTY0LjUgMzIuNXQtNDkuNSAxMTQuNXEwIDMyIDUgNzAuNXQxMC41IDcyLjV0NS41IDU0djIyMHEtMzQgLTkgLTY5LjUgLTE0dC03MS41IC01cS0xMzYgMCAtMjUxLjUgNjJ0LTE5MSAxNjl0LTkyLjUgMjQxcS05MCAxMjAgLTkwIDI2NnEwIDEwOCA0OC41IDIwMC41dDEzMiAxNTUuNXQxODguNSA4MXExNSA5OSAxMDAuNSAxODAuNXQyMTcgMTMwLjV0MjgyLjUgNDlxMTM2IDAgMjU2LjUgLTQ2IHQyMDkgLTEyNy41dDEyOC41IC0xODkuNXExNDkgLTgyIDIyNyAtMjEzLjV0NzggLTI5OS41cTAgLTEzNiAtNTggLTI0NnQtMTY1LjUgLTE4NC41dC0yNTYuNSAtMTAzLjVsLTI0MyAtMzAwdi01MnEwIC0yNyAzLjUgLTU2LjV0Ni41IC01Ny41dDMgLTUycTAgLTg1IC00MS41IC0xMTguNXQtMTU3LjUgLTMzLjV6TTEzMjUgLTI2MHE3NyAwIDk4IDE0LjV0MjEgNTcuNXEwIDI5IC0zIDY4dC02LjUgNzN0LTMuNSA0OHY2NGwyMDcgMjQ5IHEtMzEgMCAtNjAgNS41dC01NCAxMi41bC0xMDQgLTEyM3EtMSAzNCAtMiA2My41dC0xIDU0LjVxMCA2OSA5IDEyM2wzMSAyMDBsLTExNSAtMjhsLTQ2IC0yNzFsLTIwNSAyMjZxLTE5IC0xNSAtNDMgLTI4LjV0LTU1IC0yNi41bDIxOSAtMjQydi0yNzZxMCAtMjAgLTUuNSAtNjB0LTEwLjUgLTc5dC01IC01OHEwIC00MCAzMCAtNTMuNXQxMDQgLTEzLjV6TTEyNjIgNjE2cS0xMTkgMCAtMjI5LjUgMzQuNXQtMTkzLjUgOTYuNWw0OCA2NCBxNzMgLTU1IDE3MC41IC04NXQyMDQuNSAtMzBxMTM3IDAgMjQ5IDQ1LjV0MTc5IDEyMXQ2NyAxNjUuNWg4MHEwIC0xMTQgLTc3LjUgLTIwNy41dC0yMDggLTE0OXQtMjg5LjUgLTU1LjV6TTgwMyA1OTVxODAgMCAxNDkgMjkuNXQxMDggNzIuNWwyMjEgLTY3bDMwOSA4NnE0NyAtMzIgMTA0LjUgLTUwdDExNy41IC0xOHE5MSAwIDE2NSAzOHQxMTguNSAxMDMuNXQ0NC41IDE0Ni41cTAgNzYgLTM0LjUgMTQ5dC05NS41IDEzNHQtMTQzIDk5IHEtMzcgMTA3IC0xMTUuNSAxODMuNXQtMTg2IDExNy41dC0yMzAuNSA0MXEtMTAzIDAgLTE5Ny41IC0yNnQtMTY5IC03Mi41dC0xMTcuNSAtMTA4dC00MyAtMTMxLjVxMCAtMzQgMTQuNSAtNjIuNXQ0MC41IC01MC41bC01NSAtNTlxLTM0IDI5IC01NCA2NS41dC0yNSA4MS41cS04MSAtMTggLTE0NSAtNzB0LTEwMSAtMTI1LjV0LTM3IC0xNTguNXEwIC0xMDIgNDguNSAtMTgwLjV0MTI5LjUgLTEyM3QxNzkgLTQ0LjV6Ii8+PC9zdmc+"></a>
36
37
  <a href="https://pypi.python.org/pypi/pdoc"><img height="20" alt="PyPI Version" src="https://shields.mitmproxy.org/pypi/v/pdoc.svg"></a>
@@ -8,7 +8,6 @@ astunparse
8
8
  [dev]
9
9
  tox
10
10
  ruff
11
- black
12
11
  mypy
13
12
  types-pygments
14
13
  pytest
@@ -16,3 +15,4 @@ pytest-cov
16
15
  pytest-timeout
17
16
  hypothesis
18
17
  pygments>=2.14.0
18
+ pdoc-pyo3-sample-library==1.0.11
@@ -27,6 +27,7 @@ classifiers = [
27
27
  "Programming Language :: Python :: 3.9",
28
28
  "Programming Language :: Python :: 3.10",
29
29
  "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
30
31
  "Typing :: Typed",
31
32
  ]
32
33
 
@@ -43,7 +44,6 @@ pdoc = "pdoc.__main__:cli"
43
44
  dev = [
44
45
  "tox",
45
46
  "ruff",
46
- "black",
47
47
  "mypy",
48
48
  "types-pygments",
49
49
  "pytest",
@@ -51,6 +51,7 @@ dev = [
51
51
  "pytest-timeout",
52
52
  "hypothesis",
53
53
  "pygments >= 2.14.0",
54
+ "pdoc-pyo3-sample-library==1.0.11",
54
55
  ]
55
56
 
56
57
  [build-system]
@@ -88,9 +89,6 @@ markers = [
88
89
  "slow: marks tests as slow.",
89
90
  ]
90
91
 
91
- [tool.black]
92
- extend-exclude = "test/testdata/demo.py"
93
-
94
92
  [[tool.mypy.overrides]]
95
93
  module = "pytest.*"
96
94
  ignore_missing_imports = true
@@ -100,8 +98,9 @@ module = "demopackage2"
100
98
  ignore_missing_imports = true
101
99
 
102
100
  [tool.ruff]
103
- line-length = 140
101
+ extend-exclude = ["test/testdata/demo.py"]
104
102
  select = ["E", "F", "I"]
103
+ ignore = ["E501"]
105
104
 
106
105
  [tool.ruff.isort]
107
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, {}, None, types.ModuleType("a"), "a") == 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", {}, None, types.ModuleType("a"), "a") == "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
- safe_eval_type(
41
- "FooFn[int]", {"typing": typing}, None, types.ModuleType("a"), "a"
42
- )
43
- == "FooFn[int]"
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"
@@ -27,6 +27,7 @@ def test_walk_specs():
27
27
  "demopackage._child_e",
28
28
  "demopackage.child_b",
29
29
  "demopackage.child_c",
30
+ "demopackage.subpackage",
30
31
  ]
31
32
  with pytest.raises(ValueError, match="No modules found matching spec: unknown"):
32
33
  with pytest.warns(UserWarning, match="Cannot find spec for unknown"):
@@ -59,6 +60,14 @@ def test_walk_specs():
59
60
  "test.mod_with_main.__main__",
60
61
  ]
61
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
+
62
71
 
63
72
  def test_parse_spec(monkeypatch):
64
73
  p = sys.path
@@ -29,14 +29,14 @@ class Snapshot:
29
29
  def __init__(
30
30
  self,
31
31
  id: str,
32
- filenames: list[str] | None = None,
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 = filenames or [f"{id}.py"]
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",
@@ -91,3 +91,9 @@ def test_get_module_mtime():
91
91
 
92
92
  def test_get_unknown():
93
93
  assert b"404 Not Found" in handle_request(b"GET /unknown HTTP/1.1\r\n\r\n")
94
+
95
+
96
+ def test_get_not_normalized():
97
+ assert b"Not Found: Please normalize all module separators" in handle_request(
98
+ b"GET /module.submodule HTTP/1.1\r\n\r\n"
99
+ )
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