Skip to content

Commit

Permalink
resolve typeddict bases recursively, fix #726 (#729)
Browse files Browse the repository at this point in the history
  • Loading branch information
mhils authored Aug 13, 2024
1 parent 14b6fc8 commit da0c1cd
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 9 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
- Fix a bug where entire modules would be excluded by `--no-include-undocumented`.
To exclude modules, see https://pdoc.dev/docs/pdoc.html#exclude-submodules-from-being-documented.
([#728](https://github.com/mitmproxy/pdoc/pull/728), @mhils)
- Fix a bug where subclasses of TypedDict subclasses would not render correctly.
([#729](https://github.com/mitmproxy/pdoc/pull/729), @mhils)

## 2024-07-24: pdoc 14.6.0

Expand Down
11 changes: 11 additions & 0 deletions pdoc/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ def format_usage(self):
return ' | '.join(self.option_strings)


if sys.version_info >= (3, 10):
from typing import is_typeddict
else: # pragma: no cover
def is_typeddict(tp):
try:
return tp.__orig_bases__[-1].__name__ == "TypedDict"
except Exception:
return False


__all__ = [
"cache",
"ast_unparse",
Expand All @@ -134,4 +144,5 @@ def format_usage(self):
"removesuffix",
"formatannotation",
"BooleanOptionalAction",
"is_typeddict",
]
25 changes: 17 additions & 8 deletions pdoc/doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from typing import Any
from typing import ClassVar
from typing import Generic
from typing import TypedDict
from typing import TypeVar
from typing import Union
from typing import get_origin
Expand All @@ -49,6 +50,7 @@
from pdoc._compat import TypeAliasType
from pdoc._compat import cache
from pdoc._compat import formatannotation
from pdoc._compat import is_typeddict
from pdoc.doc_types import GenericAlias
from pdoc.doc_types import NonUserDefinedCallables
from pdoc.doc_types import empty
Expand Down Expand Up @@ -655,14 +657,21 @@ def _var_annotations(self) -> dict[str, type]:
@cached_property
def _bases(self) -> tuple[type, ...]:
orig_bases = _safe_getattr(self.obj, "__orig_bases__", ())
old_python_typeddict_workaround = (
sys.version_info < (3, 12)
and orig_bases
and _safe_getattr(orig_bases[-1], "__name__", None) == "TypedDict"
)
if old_python_typeddict_workaround: # pragma: no cover
# TypedDicts on Python <3.12 have a botched __mro__. We need to fix it.
return (self.obj, *orig_bases[:-1])

if is_typeddict(self.obj):
if sys.version_info < (3, 12): # pragma: no cover
# TypedDicts on Python <3.12 have a botched __mro__. We need to fix it.
return (self.obj, *orig_bases[:-1])
else:
# TypedDict on Python >=3.12 removes intermediate classes from __mro__,
# so we use orig_bases to recover the full mro.
while orig_bases and orig_bases[-1] is not TypedDict:
parent_bases = _safe_getattr(orig_bases[-1], "__orig_bases__", ())
if (
len(parent_bases) != 1 or parent_bases in orig_bases
): # sanity check that things look right
break # pragma: no cover
orig_bases = (*orig_bases, parent_bases[0])

# __mro__ and __orig_bases__ differ between Python versions and special cases like TypedDict/NamedTuple.
# This here is a pragmatic approximation of what we want.
Expand Down
4 changes: 3 additions & 1 deletion test/test_doc_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,7 @@ def get_source(mod):
monkeypatch.setitem(sys.modules, "a", a)
monkeypatch.setitem(sys.modules, "b", b)

with pytest.warns(UserWarning, match="Recursion error when importing a"):
with pytest.warns(
UserWarning, match="Recursion error when importing a|Import of xyz failed"
):
assert safe_eval_type("xyz", a.__dict__, None, a, "a") == "xyz"
81 changes: 81 additions & 0 deletions test/testdata/misc_py312.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ <h2>API Documentation</h2>
</ul>

</li>
<li>
<a class="class" href="#Baz">Baz</a>
<ul class="memberlist">
<li>
<a class="variable" href="#Baz.d">d</a>
</li>
</ul>

</li>
</ul>


Expand Down Expand Up @@ -148,6 +157,13 @@ <h1 class="modulename">
</span><span id="L-52"><a href="#L-52"><span class="linenos">52</span></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;Second attribute.&quot;&quot;&quot;</span>
</span><span id="L-53"><a href="#L-53"><span class="linenos">53</span></a> <span class="n">c</span><span class="p">:</span> <span class="nb">str</span>
</span><span id="L-54"><a href="#L-54"><span class="linenos">54</span></a> <span class="c1"># undocumented attribute</span>
</span><span id="L-55"><a href="#L-55"><span class="linenos">55</span></a>
</span><span id="L-56"><a href="#L-56"><span class="linenos">56</span></a>
</span><span id="L-57"><a href="#L-57"><span class="linenos">57</span></a><span class="k">class</span> <span class="nc">Baz</span><span class="p">(</span><span class="n">Bar</span><span class="p">):</span>
</span><span id="L-58"><a href="#L-58"><span class="linenos">58</span></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;A TypedDict subsubclass.&quot;&quot;&quot;</span>
</span><span id="L-59"><a href="#L-59"><span class="linenos">59</span></a>
</span><span id="L-60"><a href="#L-60"><span class="linenos">60</span></a> <span class="n">d</span><span class="p">:</span> <span class="nb">bool</span>
</span><span id="L-61"><a href="#L-61"><span class="linenos">61</span></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;new attribute&quot;&quot;&quot;</span>
</span></pre></div>


Expand Down Expand Up @@ -407,6 +423,71 @@ <h5>Inherited Members</h5>
</dl>
</div>
</section>
<section id="Baz">
<input id="Baz-view-source" class="view-source-toggle-state" type="checkbox" aria-hidden="true" tabindex="-1">
<div class="attr class">

<span class="def">class</span>
<span class="name">Baz</span><wbr>(<span class="base"><a href="#Bar">Bar</a></span>):

<label class="view-source-button" for="Baz-view-source"><span>View Source</span></label>

</div>
<a class="headerlink" href="#Baz"></a>
<div class="pdoc-code codehilite"><pre><span></span><span id="Baz-58"><a href="#Baz-58"><span class="linenos">58</span></a><span class="k">class</span> <span class="nc">Baz</span><span class="p">(</span><span class="n">Bar</span><span class="p">):</span>
</span><span id="Baz-59"><a href="#Baz-59"><span class="linenos">59</span></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;A TypedDict subsubclass.&quot;&quot;&quot;</span>
</span><span id="Baz-60"><a href="#Baz-60"><span class="linenos">60</span></a>
</span><span id="Baz-61"><a href="#Baz-61"><span class="linenos">61</span></a> <span class="n">d</span><span class="p">:</span> <span class="nb">bool</span>
</span><span id="Baz-62"><a href="#Baz-62"><span class="linenos">62</span></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;new attribute&quot;&quot;&quot;</span>
</span></pre></div>


<div class="docstring"><p>A TypedDict subsubclass.</p>
</div>


<div id="Baz.d" class="classattr">
<div class="attr variable">
<span class="name">d</span><span class="annotation">: bool</span>


</div>
<a class="headerlink" href="#Baz.d"></a>

<div class="docstring"><p>new attribute</p>
</div>


</div>
<div class="inherited">
<h5>Inherited Members</h5>
<dl>
<div><dt><a href="#Bar">Bar</a></dt>
<dd id="Baz.b" class="variable"><a href="#Bar.b">b</a></dd>
<dd id="Baz.c" class="variable"><a href="#Bar.c">c</a></dd>

</div>
<div><dt><a href="#Foo">Foo</a></dt>
<dd id="Baz.a" class="variable"><a href="#Foo.a">a</a></dd>

</div>
<div><dt>builtins.dict</dt>
<dd id="Baz.get" class="function">get</dd>
<dd id="Baz.setdefault" class="function">setdefault</dd>
<dd id="Baz.pop" class="function">pop</dd>
<dd id="Baz.popitem" class="function">popitem</dd>
<dd id="Baz.keys" class="function">keys</dd>
<dd id="Baz.items" class="function">items</dd>
<dd id="Baz.values" class="function">values</dd>
<dd id="Baz.update" class="function">update</dd>
<dd id="Baz.fromkeys" class="function">fromkeys</dd>
<dd id="Baz.clear" class="function">clear</dd>
<dd id="Baz.copy" class="function">copy</dd>

</div>
</dl>
</div>
</section>
</main>
</body>
</html>
7 changes: 7 additions & 0 deletions test/testdata/misc_py312.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,10 @@ class Bar(Foo, total=False):
"""Second attribute."""
c: str
# undocumented attribute


class Baz(Bar):
"""A TypedDict subsubclass."""

d: bool
"""new attribute"""
17 changes: 17 additions & 0 deletions test/testdata/misc_py312.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,21 @@
<method def clear(unknown): ... # inherited from builtins.dict.clear, D.clear() -> None. …>
<method def copy(unknown): ... # inherited from builtins.dict.copy, D.copy() -> a shallo…>
>
<class misc_py312.Baz # A TypedDict subsubcl…
<var b: int # inherited from misc_py312.Bar.b, Second attribute.>
<var c: str # inherited from misc_py312.Bar.c>
<var a: Optional[int] # inherited from misc_py312.Foo.a, First attribute.>
<var d: bool # new attribute>
<method def get(self, key, default=None, /): ... # inherited from builtins.dict.get, Return the value for…>
<method def setdefault(self, key, default=None, /): ... # inherited from builtins.dict.setdefault, Insert key with a va…>
<method def pop(unknown): ... # inherited from builtins.dict.pop, D.pop(k[,d]) -> v, r…>
<method def popitem(self, /): ... # inherited from builtins.dict.popitem, Remove and return a …>
<method def keys(unknown): ... # inherited from builtins.dict.keys, D.keys() -> a set-li…>
<method def items(unknown): ... # inherited from builtins.dict.items, D.items() -> a set-l…>
<method def values(unknown): ... # inherited from builtins.dict.values, D.values() -> an obj…>
<method def update(unknown): ... # inherited from builtins.dict.update, D.update([E, ]**F) -…>
<method def fromkeys(type, iterable, value=None, /): ... # inherited from builtins.dict.fromkeys, Create a new diction…>
<method def clear(unknown): ... # inherited from builtins.dict.clear, D.clear() -> None. …>
<method def copy(unknown): ... # inherited from builtins.dict.copy, D.copy() -> a shallo…>
>
>

0 comments on commit da0c1cd

Please sign in to comment.