From dbc57fb898b3f73d8c496b8957d61210e1f5ba12 Mon Sep 17 00:00:00 2001 From: Italo Silva Date: Fri, 13 Mar 2026 20:40:36 +0000 Subject: [PATCH 1/2] Add 'View Source' to global variables --- pdoc/doc.py | 17 ++ pdoc/doc_ast.py | 12 +- test/testdata/collections_abc.html | 11 +- test/testdata/demo.html | 22 ++- test/testdata/demo_long.html | 187 ++++++++++++++++------ test/testdata/demopackage.html | 11 +- test/testdata/demopackage_dir.html | 44 +++-- test/testdata/enums.html | 99 ++++++++---- test/testdata/example_customtemplate.html | 22 ++- test/testdata/example_darkmode.html | 22 ++- test/testdata/example_mkdocs.html | 22 ++- test/testdata/flavors_google.html | 22 ++- test/testdata/flavors_numpy.html | 22 ++- test/testdata/mermaid_demo.html | 22 ++- test/testdata/misc.html | 55 +++++-- test/testdata/misc_py310.html | 22 ++- test/testdata/misc_py312.html | 66 +++++--- test/testdata/type_checking_imports.html | 33 ++-- test/testdata/type_stubs.html | 33 ++-- test/testdata/typed_dict.html | 44 +++-- test/testdata/with_pydantic.html | 22 ++- 21 files changed, 596 insertions(+), 214 deletions(-) diff --git a/pdoc/doc.py b/pdoc/doc.py index 41c0d673..bb8c0e8a 100644 --- a/pdoc/doc.py +++ b/pdoc/doc.py @@ -327,6 +327,23 @@ def members(self) -> dict[str, Doc]: taken_from=taken_from, ) + if isinstance(doc, Variable) and not is_property: + _ast_info = doc_ast.walk_tree(self.obj) + if name in _ast_info.var_source_lines: + start, end = _ast_info.var_source_lines[name] + parent_source = doc_ast.get_source(self.obj) + if parent_source: + src_lines = parent_source.splitlines(True) + doc.source = "".join(src_lines[start - 1 : end]) + parent_start = ( + self.source_lines[0] if self.source_lines else 1 + ) + doc.source_lines = ( + parent_start + start - 1, + parent_start + end - 1, + ) + doc.source_file = self.source_file + if _doc := _pydantic.get_field_docstring(cast(type, self.obj), name): doc.docstring = _doc elif self._var_docstrings.get(name): diff --git a/pdoc/doc_ast.py b/pdoc/doc_ast.py index 0d68adc5..3d08ad33 100644 --- a/pdoc/doc_ast.py +++ b/pdoc/doc_ast.py @@ -93,8 +93,12 @@ class AstInfo: """A qualname -> docstring mapping for functions.""" annotations: dict[str, str | type[pdoc.doc_types.empty]] """A qualname -> annotation mapping. - + Annotations are not evaluated by this module and only returned as strings.""" + var_source_lines: dict[str, tuple[int, int]] + """A qualname -> (start_line, end_line) mapping for variable assignments. + + Line numbers are 1-based and relative to the parsed source.""" def walk_tree(obj: types.ModuleType | type) -> AstInfo: @@ -111,6 +115,7 @@ def _walk_tree( var_docstrings = {} func_docstrings = {} annotations = {} + var_source_lines: dict[str, tuple[int, int]] = {} for a, b in _pairwise_longest(_nodes(tree)): if isinstance(a, ast_TypeAlias): name = a.name.id @@ -139,6 +144,10 @@ def _walk_tree( continue else: continue + # Record source line info for variables (skip synthetic nodes from _init_nodes) + lineno = getattr(a, "lineno", None) + if lineno is not None: + var_source_lines[name] = (lineno, getattr(a, "end_lineno", None) or lineno) if ( isinstance(b, ast.Expr) and isinstance(b.value, ast.Constant) @@ -149,6 +158,7 @@ def _walk_tree( var_docstrings, func_docstrings, annotations, + var_source_lines, ) diff --git a/test/testdata/collections_abc.html b/test/testdata/collections_abc.html index 65961941..8d2184ab 100644 --- a/test/testdata/collections_abc.html +++ b/test/testdata/collections_abc.html @@ -131,14 +131,19 @@

-
+ +
var: Container[str] = 'baz' - + +
- +
21var: Container[str] = "baz"
+
+ +
diff --git a/test/testdata/demo.html b/test/testdata/demo.html index f34c7903..2bb08612 100644 --- a/test/testdata/demo.html +++ b/test/testdata/demo.html @@ -141,26 +141,36 @@

-
+ +
name: str - + +
- +
8    name: str
+
+ +

The name of our dog.

-
+ +
friends: list[Dog] - + +
- +
10    friends: list["Dog"]
+
+ +

The friends of our dog.

diff --git a/test/testdata/demo_long.html b/test/testdata/demo_long.html index 75d6e154..cb819b07 100644 --- a/test/testdata/demo_long.html +++ b/test/testdata/demo_long.html @@ -522,14 +522,19 @@

A Second Section

-
+ +
FOO_CONSTANT: int = 42 - + +
- +
37FOO_CONSTANT: int = 42
+
+ +

A happy constant. ✨ pdoc documents constants with their type annotation and default value.

@@ -537,13 +542,18 @@

A Second Section

-
+ +
FOO_SINGLETON: Foo - + +
- +
43FOO_SINGLETON: "Foo"
+
+ +

This variable is annotated with a type only, but not assigned to a value. We also haven't defined the associated type (Foo) yet, so the type annotation in the code in the source code is actually a string literal:

@@ -561,13 +571,18 @@

A Second Section

-
+ +
NO_DOCSTRING: int - + +
- +
58NO_DOCSTRING: int
+
+ +
@@ -739,27 +754,37 @@

A Second Section

-
+ +
an_attribute: str | list[int] - + +
- +
98    an_attribute: str | list["int"]
+
+ +

A regular attribute with type annotations

-
+ +
a_class_attribute: ClassVar[str] = 'lots of foo!' - + +
- +
101    a_class_attribute: ClassVar[str] = "lots of foo!"
+
+ +

An attribute with a ClassVar annotation.

@@ -990,13 +1015,18 @@

A Second Section

-
+ +
bar: str - + +
- +
150    bar: str
+
+ +

A new attribute defined on this subclass.

@@ -1223,28 +1253,38 @@
Inherited Members
-
+ +
CONST_B = 'yes' - + +
- +
203CONST_B = "yes"
+
+ +

A constant without type annotation

-
+ +
CONST_NO_DOC = 'SHOULD NOT APPEAR' - + +
- +
206CONST_NO_DOC = "SHOULD NOT APPEAR"
+
+ +
@@ -1300,62 +1340,87 @@
Inherited Members
-
+ +
a: int - + +
- +
218    a: int
+
+ +

Again, we can document individual properties with docstrings.

-
+ +
a2: Sequence[str] - + +
- +
220    a2: Sequence[str]
+
+ +
-
+ +
a3 = 'a3' - + +
- +
222    a3 = "a3"
+
+ +
-
+ +
a4: str = 'a4' - + +
- +
224    a4: str = "a4"
+
+ +
-
+ +
b: bool = True - + +
- +
226    b: bool = field(repr=False, default=True)
+
+ +

This property is assigned to dataclasses.field(), which works just as well.

@@ -1396,14 +1461,19 @@
Inherited Members
-
+ +
c: str = '42' - + +
- +
232    c: str = "42"
+
+ +

A new attribute.

@@ -1456,42 +1526,57 @@
Inherited Members
-
+ +
RED = <EnumDemo.RED: 1> - + +
- +
243    RED = 1
+
+ +

I am the red.

-
+ +
GREEN = <EnumDemo.GREEN: 2> - + +
- +
245    GREEN = 2
+
+ +

I am green.

-
+ +
BLUE = <EnumDemo.BLUE: 3> - + +
- +
247    BLUE = enum.auto()
+
+ +
diff --git a/test/testdata/demopackage.html b/test/testdata/demopackage.html index e42452a3..f476dc41 100644 --- a/test/testdata/demopackage.html +++ b/test/testdata/demopackage.html @@ -212,13 +212,18 @@

-
+ +
b_type: Type[B] - + +
- +
12    b_type: typing.Type[B]
+
+ +

we have a self-referential attribute here

diff --git a/test/testdata/demopackage_dir.html b/test/testdata/demopackage_dir.html index c6c3c9f0..1ff44238 100644 --- a/test/testdata/demopackage_dir.html +++ b/test/testdata/demopackage_dir.html @@ -344,14 +344,19 @@

-
+ +
x = 42 - + +
- +
3x = 42
+
+ +
@@ -765,13 +770,18 @@

-
+ +
b_type: Type[B] - + +
- +
12    b_type: typing.Type[B]
+
+ +

we have a self-referential attribute here

@@ -1300,13 +1310,18 @@

-
+ +
b_type: Type[B] - + +
- +
12    b_type: typing.Type[B]
+
+ +

we have a self-referential attribute here

@@ -2362,13 +2377,18 @@

-
+ +
b_type: Type[B] - + +
- +
12    b_type: typing.Type[B]
+
+ +

we have a self-referential attribute here

diff --git a/test/testdata/enums.html b/test/testdata/enums.html index 2e8fef7f..4d73a5fd 100644 --- a/test/testdata/enums.html +++ b/test/testdata/enums.html @@ -169,42 +169,57 @@

-
+ +
RED = <EnumDemo.RED: 1> - + +
- +
12    RED = 1
+
+ +

I am the red.

-
+ +
GREEN = <EnumDemo.GREEN: 2> - + +
- +
14    GREEN = 2
+
+ +

I am green.

-
+ +
BLUE = <EnumDemo.BLUE: 3> - + +
- +
16    BLUE = enum.auto()
+
+ +
@@ -229,26 +244,36 @@

-
+ +
FOO = <EnumWithoutDocstrings.FOO: 1> - + +
- +
20    FOO = enum.auto()
+
+ +
-
+ +
BAR = <EnumWithoutDocstrings.BAR: 2> - + +
- +
21    BAR = enum.auto()
+
+ +
@@ -273,26 +298,36 @@

-
+ +
FOO = <IntEnum.FOO: 1> - + +
- +
25    FOO = enum.auto()
+
+ +
-
+ +
BAR = <IntEnum.BAR: 2> - + +
- +
26    BAR = enum.auto()
+
+ +
@@ -321,26 +356,36 @@

-
+ +
FOO = <StrEnum.FOO: 'foo'> - + +
- +
30    FOO = enum.auto()
+
+ +
-
+ +
BAR = <StrEnum.BAR: 'bar'> - + +
- +
31    BAR = enum.auto()
+
+ +
diff --git a/test/testdata/example_customtemplate.html b/test/testdata/example_customtemplate.html index 76212e43..b0f7c0d5 100644 --- a/test/testdata/example_customtemplate.html +++ b/test/testdata/example_customtemplate.html @@ -143,26 +143,36 @@

-
+ +
name: str - + +
- +
8    name: str
+
+ +

The name of our dog.

-
+ +
friends: list[Dog] - + +
- +
10    friends: list["Dog"]
+
+ +

The friends of our dog.

diff --git a/test/testdata/example_darkmode.html b/test/testdata/example_darkmode.html index 947149e9..10155d05 100644 --- a/test/testdata/example_darkmode.html +++ b/test/testdata/example_darkmode.html @@ -141,26 +141,36 @@

-
+ +
name: str - + +
- +
8    name: str
+
+ +

The name of our dog.

-
+ +
friends: list[Dog] - + +
- +
10    friends: list["Dog"]
+
+ +

The friends of our dog.

diff --git a/test/testdata/example_mkdocs.html b/test/testdata/example_mkdocs.html index 9f863572..b5bd644a 100644 --- a/test/testdata/example_mkdocs.html +++ b/test/testdata/example_mkdocs.html @@ -92,26 +92,36 @@

-
+ +
name: str - + +
- +
8    name: str
+
+ +

The name of our dog.

-
+ +
friends: list[Dog] - + +
- +
10    friends: list["Dog"]
+
+ +

The friends of our dog.

diff --git a/test/testdata/flavors_google.html b/test/testdata/flavors_google.html index 105b808a..a2d03355 100644 --- a/test/testdata/flavors_google.html +++ b/test/testdata/flavors_google.html @@ -668,26 +668,36 @@
Todo:
-
+ +
module_level_variable1 = 12345 - + +
- +
48module_level_variable1 = 12345
+
+ +
-
+ +
module_level_variable2 = 98765 - + +
- +
50module_level_variable2 = 98765
+
+ +

int: Module level variable documented inline.

The docstring may span multiple lines. The type may optionally be specified diff --git a/test/testdata/flavors_numpy.html b/test/testdata/flavors_numpy.html index 53d7b846..4a0112d8 100644 --- a/test/testdata/flavors_numpy.html +++ b/test/testdata/flavors_numpy.html @@ -668,26 +668,36 @@

Attributes
-
+ +
module_level_variable1 = 12345 - + +
- +
56module_level_variable1 = 12345
+
+ +
-
+ +
module_level_variable2 = 98765 - + +
- +
58module_level_variable2 = 98765
+
+ +

int: Module level variable documented inline.

The docstring may span multiple lines. The type may optionally be specified diff --git a/test/testdata/mermaid_demo.html b/test/testdata/mermaid_demo.html index f8241f5c..3f5b9a5a 100644 --- a/test/testdata/mermaid_demo.html +++ b/test/testdata/mermaid_demo.html @@ -186,26 +186,36 @@

-
+ +
name: str - + +
- +
21    name: str
+
+ +

The name of our pet.

-
+ +
friends: list[Pet] - + +
- +
23    friends: list["Pet"]
+
+ +

The friends of our pet.

diff --git a/test/testdata/misc.html b/test/testdata/misc.html index 90544063..7fecc2d8 100644 --- a/test/testdata/misc.html +++ b/test/testdata/misc.html @@ -908,14 +908,19 @@

-
+ +
var_with_default_obj = <object object> - + +
- +
40var_with_default_obj = default_obj
+
+ +

this shouldn't render the object address

@@ -1248,14 +1253,19 @@

-
+ +
quuux: int = 42 - + +
- +
146    quuux: int = 42
+
+ +

quuux

@@ -1263,13 +1273,18 @@

-
+ +
only_annotated: int - + +
- +
150only_annotated: int
+
+ +
@@ -2250,28 +2265,38 @@
Heading 6
-
+ +
static_attr_to_class = <class 'ClassDecorator'> - + +
- +
392    static_attr_to_class = ClassDecorator
+
+ +

this is a static attribute that point to a Class (not an instance)

-
+ +
static_attr_to_instance = <ClassDecorator object> - + +
- +
395    static_attr_to_instance = ClassDecorator(None)
+
+ +

this is a static attribute that point to an instance

diff --git a/test/testdata/misc_py310.html b/test/testdata/misc_py310.html index 1a140e18..e54b8649 100644 --- a/test/testdata/misc_py310.html +++ b/test/testdata/misc_py310.html @@ -120,28 +120,38 @@

-
+ +
NewStyleDict = dict[str, str] - + +
- +
12NewStyleDict = dict[str, str]
+
+ +

New-style dict.

-
+ +
OldStyleDict = typing.Dict[str, str] - + +
- +
15OldStyleDict = Dict[str, str]
+
+ +

Old-style dict.

diff --git a/test/testdata/misc_py312.html b/test/testdata/misc_py312.html index 4a3e86b7..87e31e28 100755 --- a/test/testdata/misc_py312.html +++ b/test/testdata/misc_py312.html @@ -115,53 +115,73 @@

-
+ +
type MyType = int - + +
- +
13type MyType = int
+
+ +

A custom Python 3.12 type.

-
+ +
foo: MyType - + +
- +
16foo: MyType
+
+ +

A custom type instance.

-
+ +
type MyTypeWithoutDocstring = int - + +
- +
20type MyTypeWithoutDocstring = int
+
+ +
-
+ +
MyTypeClassic: TypeAlias = int - + +
- +
22MyTypeClassic: typing.TypeAlias = int
+
+ +

A "classic" typing.TypeAlias.

@@ -208,26 +228,36 @@

-
+ +
name: str - + +
- +
36    name: str
+
+ +

Name of our example tuple.

-
+ +
id: int - + +
- +
38    id: int = 3
+
+ +

Alias for field number 1

diff --git a/test/testdata/type_checking_imports.html b/test/testdata/type_checking_imports.html index f437f75e..12b7cef0 100755 --- a/test/testdata/type_checking_imports.html +++ b/test/testdata/type_checking_imports.html @@ -122,28 +122,38 @@

-
+ +
var: Sequence[int] = (1, 2, 3) - + +
- +
24var: Sequence[int] = (1, 2, 3)
+
+ +

A variable with TYPE_CHECKING type annotations.

-
+ +
imported_from_cached_module: str | int = 42 - + +
- +
28imported_from_cached_module: StrOrInt = 42
+
+ +

A variable with a type annotation that's imported from another file's TYPE_CHECKING block.

https://github.com/mitmproxy/pdoc/issues/648

@@ -152,14 +162,19 @@

-
+ +
imported_from_uncached_module: str | bool = True - + +
- +
35imported_from_uncached_module: StrOrBool = True
+
+ +

A variable with a type annotation that's imported from another file's TYPE_CHECKING block.

In this case, the module is not in sys.modules outside of TYPE_CHECKING.

diff --git a/test/testdata/type_stubs.html b/test/testdata/type_stubs.html index 03d4e941..a3ab5d48 100644 --- a/test/testdata/type_stubs.html +++ b/test/testdata/type_stubs.html @@ -156,14 +156,19 @@

-
+ +
var: list[str] = [] - + +
- +
12var = []
+
+ +

Docstring override from the .pyi file.

@@ -205,14 +210,19 @@

-
+ +
attr: int = 42 - + +
- +
18    attr = 42
+
+ +

An attribute

@@ -305,14 +315,19 @@

-
+ +
attr: str = '42' - + +
- +
25        attr = "42"
+
+ +

An attribute

diff --git a/test/testdata/typed_dict.html b/test/testdata/typed_dict.html index f823d3cd..dc826823 100644 --- a/test/testdata/typed_dict.html +++ b/test/testdata/typed_dict.html @@ -120,13 +120,18 @@

-
+ +
a: int | None - + +
- +
6    a: int | None
+
+ +

First attribute.

@@ -159,26 +164,36 @@

-
+ +
b: int - + +
- +
13    b: int
+
+ +

Second attribute.

-
+ +
c: str - + +
- +
15    c: str
+
+ +
@@ -216,13 +231,18 @@
Inherited Members
-
+ +
d: bool - + +
- +
22    d: bool
+
+ +

new attribute

diff --git a/test/testdata/with_pydantic.html b/test/testdata/with_pydantic.html index ff980df6..bd6b4f0e 100644 --- a/test/testdata/with_pydantic.html +++ b/test/testdata/with_pydantic.html @@ -110,28 +110,38 @@

-
+ +
a: int = 1 - + +
- +
16    a: int = pydantic.Field(default=1, description="Docstring for a")
+
+ +

Docstring for a

-
+ +
b: int = 2 - + +
- +
18    b: int = 2
+
+ +

Docstring for b.

From acc06f7c61128c975f7d14d35b891960c62ea85f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:54:56 +0000 Subject: [PATCH 2/2] [autofix.ci] apply automated fixes --- pdoc/doc.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pdoc/doc.py b/pdoc/doc.py index bb8c0e8a..5d3e5295 100644 --- a/pdoc/doc.py +++ b/pdoc/doc.py @@ -335,9 +335,7 @@ def members(self) -> dict[str, Doc]: if parent_source: src_lines = parent_source.splitlines(True) doc.source = "".join(src_lines[start - 1 : end]) - parent_start = ( - self.source_lines[0] if self.source_lines else 1 - ) + parent_start = self.source_lines[0] if self.source_lines else 1 doc.source_lines = ( parent_start + start - 1, parent_start + end - 1,