diff --git a/CHANGELOG.md b/CHANGELOG.md index 81a125bb..ef4f9d3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - Imports in a TYPE_CHECKING section that reference members defined in another module's TYPE_CHECKING section now work correctly. ([#649](https://github.com/mitmproxy/pdoc/pull/649), @mhils) + - pdoc now supports Python 3.12's `type` statements and has improved `TypeAlias` rendering. + ([#651](https://github.com/mitmproxy/pdoc/pull/651), @mhils) - Add support for `code-block` ReST directives ([#624](https://github.com/mitmproxy/pdoc/pull/624), @JCGoran) - If a variable's value meets certain entropy criteria and matches an environment variable value, diff --git a/pdoc/_compat.py b/pdoc/_compat.py index 8581d78c..c1271e39 100644 --- a/pdoc/_compat.py +++ b/pdoc/_compat.py @@ -16,6 +16,24 @@ def ast_unparse(t): # type: ignore return _unparse(t).strip("\t\n \"'") +if sys.version_info >= (3, 12): + from ast import TypeAlias as ast_TypeAlias +else: # pragma: no cover + class ast_TypeAlias: + pass + +if sys.version_info >= (3, 12): + from typing import TypeAliasType +else: # pragma: no cover + class TypeAliasType: + """Placeholder class for TypeAliasType""" + +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: # pragma: no cover + class TypeAlias: + pass + if sys.version_info >= (3, 9): from types import GenericAlias else: # pragma: no cover @@ -108,6 +126,9 @@ def format_usage(self): __all__ = [ "cache", "ast_unparse", + "ast_TypeAlias", + "TypeAliasType", + "TypeAlias", "GenericAlias", "UnionType", "removesuffix", diff --git a/pdoc/doc.py b/pdoc/doc.py index 139446a9..cbe2f8fe 100644 --- a/pdoc/doc.py +++ b/pdoc/doc.py @@ -44,15 +44,16 @@ from pdoc import doc_ast from pdoc import doc_pyi from pdoc import extract +from pdoc._compat import TypeAlias +from pdoc._compat import TypeAliasType +from pdoc._compat import cache +from pdoc._compat import formatannotation from pdoc.doc_types import GenericAlias from pdoc.doc_types import NonUserDefinedCallables from pdoc.doc_types import empty from pdoc.doc_types import resolve_annotations from pdoc.doc_types import safe_eval_type -from ._compat import cache -from ._compat import formatannotation - def _include_fullname_in_traceback(f): """ @@ -1089,6 +1090,11 @@ def is_typevar(self) -> bool: else: return False + @cached_property + def is_type_alias_type(self) -> bool: + """`True` if the variable is a `typing.TypeAliasType`, `False` otherwise.""" + return isinstance(self.default_value, TypeAliasType) + @cached_property def is_enum_member(self) -> bool: """`True` if the variable is an enum member, `False` otherwise.""" @@ -1102,6 +1108,10 @@ def default_value_str(self) -> str: """The variable's default value as a pretty-printed str.""" if self.default_value is empty: return "" + if isinstance(self.default_value, TypeAliasType): + return formatannotation(self.default_value.__value__) + elif self.annotation == TypeAlias: + return formatannotation(self.default_value) # This is not perfect, but a solid attempt at preventing accidental leakage of secrets. # If you have input on how to improve the heuristic, please send a pull request! diff --git a/pdoc/doc_ast.py b/pdoc/doc_ast.py index f91e1397..56c91496 100644 --- a/pdoc/doc_ast.py +++ b/pdoc/doc_ast.py @@ -21,6 +21,7 @@ import pdoc +from ._compat import ast_TypeAlias from ._compat import ast_unparse from ._compat import cache @@ -115,7 +116,11 @@ def _walk_tree( func_docstrings = {} annotations = {} for a, b in _pairwise_longest(_nodes(tree)): - if isinstance(a, ast.AnnAssign) and isinstance(a.target, ast.Name) and a.simple: + if isinstance(a, ast_TypeAlias): + name = a.name.id + elif ( + isinstance(a, ast.AnnAssign) and isinstance(a.target, ast.Name) and a.simple + ): name = a.target.id annotations[name] = unparse(a.annotation) elif ( @@ -183,6 +188,8 @@ def sort_by_source( name = a.target.id elif isinstance(a, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): name = a.name + elif isinstance(a, ast_TypeAlias): + name = a.name.id else: continue diff --git a/pdoc/templates/default/module.html.jinja2 b/pdoc/templates/default/module.html.jinja2 index 00382185..09cbbfc3 100644 --- a/pdoc/templates/default/module.html.jinja2 +++ b/pdoc/templates/default/module.html.jinja2 @@ -177,6 +177,7 @@ See https://pdoc.dev/docs/pdoc/render_helpers.html#DefaultMacroExtension for an {% endif %} {% enddefaultmacro %} {% defaultmacro variable(var) -%} + {%- if var.is_type_alias_type %}type {% endif -%} {{ var.name }}{{ annotation(var) }}{{ default_value(var) }} {% enddefaultmacro %} {% defaultmacro submodule(mod) -%} diff --git a/test/testdata/flavors_rst.html b/test/testdata/flavors_rst.html index c78b0ee1..e16e34bb 100644 --- a/test/testdata/flavors_rst.html +++ b/test/testdata/flavors_rst.html @@ -131,138 +131,141 @@
Changed in version 2.5: The spam parameter.
+This is a code block.
+
+
Deprecated since version 3.1:
Use spam()
instead.
53def seealso(): -54 # this is not properly supported yet -55 """ -56 .. seealso:: -57 -58 Module :py:mod:`zipfile` -59 Documentation of the :py:mod:`zipfile` standard module. +@@ -456,11 +465,11 @@56def seealso(): +57 # this is not properly supported yet +58 """ +59 .. seealso:: 60 -61 `GNU tar manual, Basic Tar Format <http://link>`_ -62 Documentation for tar archive files, including GNU tar extensions. -63 """ +61 Module :py:mod:`zipfile` +62 Documentation of the :py:mod:`zipfile` standard module. +63 +64 `GNU tar manual, Basic Tar Format <http://link>`_ +65 Documentation for tar archive files, including GNU tar extensions. +66 """This warning has a title only.
66def seealso_short(): -67 # this is not properly supported yet -68 """ -69 .. seealso:: modules :py:mod:`zipfile`, :py:mod:`tarfile` -70 """ +@@ -480,13 +489,13 @@69def seealso_short(): +70 # this is not properly supported yet +71 """ +72 .. seealso:: modules :py:mod:`zipfile`, :py:mod:`tarfile` +73 """This warning has a title only.
73def tables(): -74 """ -75 | Header 1 | *Header* 2 | -76 | -------- | -------- | -77 | `Cell 1` | [Cell 2](http://example.com) link | -78 | Cell 3 | **Cell 4** | -79 """ +@@ -523,17 +532,17 @@76def tables(): +77 """ +78 | Header 1 | *Header* 2 | +79 | -------- | -------- | +80 | `Cell 1` | [Cell 2](http://example.com) link | +81 | Cell 3 | **Cell 4** | +82 """This warning has a title only.
82def footnote1(): -83 """ -84 Cite the relevant literature, e.g. [1]_. You may also cite these -85 references in the notes section above. -86 -87 .. [1] O. McNoleg, "The integration of GIS, remote sensing, -88 expert systems and adaptive co-kriging for environmental habitat -89 modelling of the Highland Haggis using object-oriented, fuzzy-logic -90 and neural-network techniques," Computers & Geosciences, vol. 22, -91 pp. 585-588, 1996. -92 """ +@@ -567,22 +576,22 @@85def footnote1(): +86 """ +87 Cite the relevant literature, e.g. [1]_. You may also cite these +88 references in the notes section above. +89 +90 .. [1] O. McNoleg, "The integration of GIS, remote sensing, +91 expert systems and adaptive co-kriging for environmental habitat +92 modelling of the Highland Haggis using object-oriented, fuzzy-logic +93 and neural-network techniques," Computers & Geosciences, vol. 22, +94 pp. 585-588, 1996. +95 """This warning has a title only.
95def footnote2(): - 96 """ - 97 Autonumbered footnotes are - 98 possible, like using [#]_ and [#]_. - 99 -100 .. [#] This is the first one. -101 .. [#] This is the second one. +@@ -628,14 +637,14 @@98def footnote2(): + 99 """ +100 Autonumbered footnotes are +101 possible, like using [#]_ and [#]_. 102 -103 They may be assigned 'autonumber -104 labels' - for instance, -105 [#fourth]_ and [#third]_. -106 -107 .. [#third] a.k.a. third_ -108 -109 .. [#fourth] a.k.a. fourth_ -110 """ +103 .. [#] This is the first one. +104 .. [#] This is the second one. +105 +106 They may be assigned 'autonumber +107 labels' - for instance, +108 [#fourth]_ and [#third]_. +109 +110 .. [#third] a.k.a. third_ +111 +112 .. [#fourth] a.k.a. fourth_ +113 """This warning has a title only.
113def footnote3(): -114 """ -115 Auto-symbol footnotes are also -116 possible, like this: [*]_ and [*]_. -117 -118 .. [*] This is the first one. -119 .. [*] This is the second one. -120 """ +@@ -669,10 +678,10 @@116def footnote3(): +117 """ +118 Auto-symbol footnotes are also +119 possible, like this: [*]_ and [*]_. +120 +121 .. [*] This is the first one. +122 .. [*] This is the second one. +123 """This warning has a title only.
123def footnote4(): -124 """ -125 There is no footnote for this reference [#]_. -126 """ + @@ -692,12 +701,12 @@This warning has a title only.
129def include(): -130 """ -131 Included from another file: -132 -133 .. include:: flavors_rst_include/include.rst -134 """ +@@ -723,19 +732,19 @@132def include(): +133 """ +134 Included from another file: +135 +136 .. include:: flavors_rst_include/include.rst +137 """This warning has a title only.
137def fields(foo: str = "foo", bar: bool = True) -> str: -138 """This method has field descriptions. -139 -140 :param foo: A string, -141 defaults to None -142 :type foo: string, optional -143 :param bar: Another -144 boolean. -145 :return: Another string, -146 or maybe `None`. -147 :rtype: A string. -148 """ -149 raise NotImplementedError +@@ -771,13 +780,13 @@140def fields(foo: str = "foo", bar: bool = True) -> str: +141 """This method has field descriptions. +142 +143 :param foo: A string, +144 defaults to None +145 :type foo: string, optional +146 :param bar: Another +147 boolean. +148 :return: Another string, +149 or maybe `None`. +150 :rtype: A string. +151 """ +152 raise NotImplementedErrorReturns
152def fields_text_after_param(foo): -153 """This method has text after the `:param` fields. -154 -155 :param foo: Some text. -156 -157 Here's some more text. -158 """ +@@ -805,14 +814,14 @@155def fields_text_after_param(foo): +156 """This method has text after the `:param` fields. +157 +158 :param foo: Some text. +159 +160 Here's some more text. +161 """Parameters
161def fields_invalid(foo: str = "foo") -> str: -162 """This method has invalid `:param` definitions. -163 -164 :param: What is this for? -165 -166 :unknown: This is an unknown field name. -167 """ -168 raise NotImplementedError +@@ -840,11 +849,11 @@164def fields_invalid(foo: str = "foo") -> str: +165 """This method has invalid `:param` definitions. +166 +167 :param: What is this for? +168 +169 :unknown: This is an unknown field name. +170 """ +171 raise NotImplementedErrorParameters
171def fields_exception(): -172 """ -173 :raises RuntimeError: Some multi-line -174 exception description. -175 """ +diff --git a/test/testdata/flavors_rst.py b/test/testdata/flavors_rst.py index 3d0c1e1a..3fc9f2d5 100644 --- a/test/testdata/flavors_rst.py +++ b/test/testdata/flavors_rst.py @@ -40,6 +40,9 @@ def admonitions(): .. versionchanged:: 2.5 The *spam* parameter. + .. code-block:: + This is a code block. + .. deprecated:: 3.1 Use :func:`spam` instead. diff --git a/test/testdata/misc_py312.html b/test/testdata/misc_py312.html old mode 100644 new mode 100755 index 50acedf5..7f2bc5ff --- a/test/testdata/misc_py312.html +++ b/test/testdata/misc_py312.html @@ -23,6 +23,18 @@174def fields_exception(): +175 """ +176 :raises RuntimeError: Some multi-line +177 exception description. +178 """API Documentation
+
- + MyType +
+- + foo +
+- + MyTypeWithoutDocstring +
+- + MyTypeClassic +
- NamedTupleExample
@@ -82,47 +94,116 @@
-
1""" - 2Testing features that either are 3.12+ only or render slightly different on 3.12. ++1# mypy: ignore-errors + 2# https://github.com/python/mypy/issues/16607 3""" - 4from __future__ import annotations - 5 - 6from typing import NamedTuple - 7from typing import Optional - 8from typing import TypedDict - 9 -10# Testing a typing.NamedTuple -11# we do not care very much about collections.namedtuple, -12# the typing version is superior for documentation and a drop-in replacement. -13 -14 -15class NamedTupleExample(NamedTuple): -16 """ -17 An example for a typing.NamedTuple. -18 """ + 4Testing features that either are 3.12+ only or render slightly different on 3.12. + 5""" + 6from __future__ import annotations + 7 + 8import typing + 9from typing import NamedTuple +10from typing import Optional +11from typing import TypedDict +12 +13# Testing the new Python 3.12 `type` statement. +14type MyType = int +15"""A custom Python 3.12 type.""" +16 +17foo: MyType +18"""A custom type instance.""" 19 -20 name: str -21 """Name of our example tuple.""" -22 id: int = 3 -23 -24 -25# Testing some edge cases in our inlined implementation of ForwardRef._evaluate in _eval_type. -26class Foo(TypedDict): -27 a: Optional[int] -28 """First attribute.""" +20 +21type MyTypeWithoutDocstring = int +22 +23MyTypeClassic: typing.TypeAlias = int +24"""A "classic" typing.TypeAlias.""" +25 +26# Testing a typing.NamedTuple +27# we do not care very much about collections.namedtuple, +28# the typing version is superior for documentation and a drop-in replacement. 29 30 -31class Bar(Foo, total=False): -32 """A TypedDict subclass. Before 3.12, TypedDict botches __mro__.""" -33 -34 b: int -35 """Second attribute.""" -36 c: str -37 # undocumented attribute +31class NamedTupleExample(NamedTuple): +32 """ +33 An example for a typing.NamedTuple. +34 """ +35 +36 name: str +37 """Name of our example tuple.""" +38 id: int = 3 +39 +40 +41# Testing some edge cases in our inlined implementation of ForwardRef._evaluate in _eval_type. +42class Foo(TypedDict): +43 a: Optional[int] +44 """First attribute.""" +45 +46 +47class Bar(Foo, total=False): +48 """A TypedDict subclass. Before 3.12, TypedDict botches __mro__.""" +49 +50 b: int +51 """Second attribute.""" +52 c: str +53 # undocumented attribute+ ++ type MyType = +int + + ++ + ++ + +A custom Python 3.12 type.
++ ++ foo: MyType + + ++ + ++ + +A custom type instance.
++ ++ type MyTypeWithoutDocstring = +int + + ++ + + + ++ + MyTypeClassic: TypeAlias = +int + + ++ + ++ + +A "classic" typing.TypeAlias.
+ @@ -134,14 +215,14 @@-
-16class NamedTupleExample(NamedTuple): -17 """ -18 An example for a typing.NamedTuple. -19 """ -20 -21 name: str -22 """Name of our example tuple.""" -23 id: int = 3 +@@ -211,9 +292,9 @@32class NamedTupleExample(NamedTuple): +33 """ +34 An example for a typing.NamedTuple. +35 """ +36 +37 name: str +38 """Name of our example tuple.""" +39 id: int = 3Inherited Members
-27class Foo(TypedDict): -28 a: Optional[int] -29 """First attribute.""" + @@ -263,13 +344,13 @@Inherited Members
32class Bar(Foo, total=False): -33 """A TypedDict subclass. Before 3.12, TypedDict botches __mro__.""" -34 -35 b: int -36 """Second attribute.""" -37 c: str -38 # undocumented attribute +diff --git a/test/testdata/misc_py312.py b/test/testdata/misc_py312.py index 5835ab23..f9d9f9fd 100755 --- a/test/testdata/misc_py312.py +++ b/test/testdata/misc_py312.py @@ -1,12 +1,28 @@ +# mypy: ignore-errors +# https://github.com/python/mypy/issues/16607 """ Testing features that either are 3.12+ only or render slightly different on 3.12. """ from __future__ import annotations +import typing from typing import NamedTuple from typing import Optional from typing import TypedDict +# Testing the new Python 3.12 `type` statement. +type MyType = int +"""A custom Python 3.12 type.""" + +foo: MyType +"""A custom type instance.""" + + +type MyTypeWithoutDocstring = int + +MyTypeClassic: typing.TypeAlias = int +"""A "classic" typing.TypeAlias.""" + # Testing a typing.NamedTuple # we do not care very much about collections.namedtuple, # the typing version is superior for documentation and a drop-in replacement. diff --git a/test/testdata/misc_py312.txt b/test/testdata/misc_py312.txt old mode 100644 new mode 100755 index 69ec2ac3..fc7d488f --- a/test/testdata/misc_py312.txt +++ b/test/testdata/misc_py312.txt @@ -1,4 +1,8 @@48class Bar(Foo, total=False): +49 """A TypedDict subclass. Before 3.12, TypedDict botches __mro__.""" +50 +51 b: int +52 """Second attribute.""" +53 c: str +54 # undocumented attribute+ + +