From 16dfc8db4482248b1af696654806906c70f11cfa Mon Sep 17 00:00:00 2001 From: SeaStar Deng <37767638+DSeaStar@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:57:11 +0000 Subject: [PATCH] Show subprocess.Popen instead of the defused wrapper in annotations. Import-time mocking replaced Popen with _PdocDefusedPopen, so evaluated annotations leaked the internal class name into generated docs. --- CHANGELOG.md | 3 +++ pdoc/extract.py | 6 ++++++ test/test_doc.py | 26 ++++++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d64bb2a6..9459b09a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ## Unreleased: pdoc next +- Display `subprocess.Popen` in type annotations instead of the internal + `_PdocDefusedPopen` import wrapper. + ([#819](https://github.com/mitmproxy/pdoc/issues/819)) - Fix incorrect ordering of `start-after`/`end-before` in RST include directives. ([#876](https://github.com/mitmproxy/pdoc/pull/876), @JohanKarlbergg) - Support Pydantic [`computed_field`](https://docs.pydantic.dev/2.0/usage/computed_fields/) descriptions ([#855](https://github.com/mitmproxy/pdoc/pull/855), @avhz) diff --git a/pdoc/extract.py b/pdoc/extract.py index 62952ff2..ec2f09b1 100644 --- a/pdoc/extract.py +++ b/pdoc/extract.py @@ -160,6 +160,12 @@ def _noop(*args, **kwargs): class _PdocDefusedPopen(subprocess.Popen): """A small wrapper around subprocess.Popen that converts most executions into no-ops.""" + # Present as subprocess.Popen in type annotations so docs don't leak this wrapper. + # https://github.com/mitmproxy/pdoc/issues/819 + __name__ = "Popen" + __qualname__ = "Popen" + __module__ = "subprocess" + if platform.system() == "Windows": # pragma: no cover _noop_exe = "echo.exe" else: # pragma: no cover diff --git a/test/test_doc.py b/test/test_doc.py index 3fcf96d4..7d761449 100644 --- a/test/test_doc.py +++ b/test/test_doc.py @@ -7,11 +7,13 @@ import pytest from pdoc import extract +from pdoc._compat import formatannotation from pdoc.doc import Class from pdoc.doc import Module from pdoc.doc import Variable from pdoc.doc import _environ_lookup from pdoc.doc_types import empty +from pdoc.extract import _PdocDefusedPopen here = Path(__file__).parent @@ -186,3 +188,27 @@ def test_source_file_method(): assert m.members["Foo"].members["a_cached_function"].source_file == ( here / "testdata" / "demo_long.py" ) + + +def test_defused_popen_annotation_displays_as_subprocess_popen(tmp_path): + """https://github.com/mitmproxy/pdoc/issues/819""" + assert formatannotation(_PdocDefusedPopen) == "subprocess.Popen" + assert formatannotation(_PdocDefusedPopen[str]) == "subprocess.Popen[str]" + + f = tmp_path / "popen_anno.py" + f.write_text( + "import subprocess\n" + "\n" + "class Bar:\n" + " foo: subprocess.Popen[str]\n" + "\n" + "def make() -> subprocess.Popen[bytes]:\n" + " raise NotImplementedError\n" + ) + mod = extract.load_module(extract.parse_spec(f)) + m = Module(mod) + assert m.members["Bar"].members["foo"].annotation_str == ": subprocess.Popen[str]" + assert ( + m.members["make"].signature._return_annotation_str() + == "subprocess.Popen[bytes]" + )