Skip to content

Commit 5d53f92

Browse files
committed
Fix all class members missing when documenting a module with the same name as a standard library module
Fixes #478
1 parent 74770d3 commit 5d53f92

File tree

11 files changed

+226
-78
lines changed

11 files changed

+226
-78
lines changed

autoapi/_astroid_utils.py

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -644,36 +644,6 @@ def get_return_annotation(node: astroid.nodes.FunctionDef) -> str | None:
644644
return return_annotation
645645

646646

647-
def get_func_docstring(node: astroid.nodes.FunctionDef) -> str:
648-
"""Get the docstring of a node, using a parent docstring if needed.
649-
650-
Args:
651-
node: The node to get a docstring for.
652-
653-
Returns:
654-
The docstring of the function, or the empty string if no docstring
655-
was found or defined.
656-
"""
657-
doc = node.doc_node.value if node.doc_node else ""
658-
659-
if not doc and isinstance(node.parent, astroid.nodes.ClassDef):
660-
for base in node.parent.ancestors():
661-
if node.name in ("__init__", "__new__"):
662-
base_module = base.qname().split(".", 1)[0]
663-
if in_stdlib(base_module):
664-
continue
665-
666-
for child in base.get_children():
667-
if (
668-
isinstance(child, node.__class__)
669-
and child.name == node.name
670-
and child.doc_node is not None
671-
):
672-
return child.doc_node.value
673-
674-
return doc
675-
676-
677647
def get_class_docstring(node: astroid.nodes.ClassDef) -> str:
678648
"""Get the docstring of a node, using a parent docstring if needed.
679649

autoapi/_mapper.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import operator
66
import os
77
import re
8+
import sys
89

910
from jinja2 import Environment, FileSystemLoader
1011
import sphinx
@@ -31,6 +32,14 @@
3132
)
3233
from .settings import OWN_PAGE_LEVELS, TEMPLATE_DIR
3334

35+
if sys.version_info < (3, 10): # PY310
36+
from stdlib_list import in_stdlib
37+
else:
38+
39+
def in_stdlib(module_name: str) -> bool:
40+
return module_name in sys.stdlib_module_names
41+
42+
3443
LOGGER = sphinx.util.logging.getLogger(__name__)
3544

3645

@@ -451,6 +460,24 @@ def read_file(self, path, **kwargs):
451460
)
452461
return None
453462

463+
def _skip_if_stdlib(self):
464+
documented_modules = {obj["full_name"] for obj in self.paths.values()}
465+
466+
q = collections.deque(self.paths.values())
467+
while q:
468+
obj = q.popleft()
469+
if "children" in obj:
470+
q.extend(obj["children"])
471+
472+
if obj.get("inherited", False):
473+
module = obj["inherited_from"]["full_name"].split(".", 1)[0]
474+
if (
475+
in_stdlib(module)
476+
and not obj["inherited_from"]["is_abstract"]
477+
and module not in documented_modules
478+
):
479+
obj["hide"] = True
480+
454481
def _resolve_placeholders(self):
455482
"""Resolve objects that have been imported from elsewhere."""
456483
modules = {}
@@ -477,6 +504,7 @@ def _hide_yo_kids(self):
477504
child["hide"] = True
478505

479506
def map(self, options=None):
507+
self._skip_if_stdlib()
480508
self._resolve_placeholders()
481509
self._hide_yo_kids()
482510
self.app.env.autoapi_annotations = {}

autoapi/_parser.py

Lines changed: 68 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import collections
2-
import itertools
32
import os
4-
import sys
53

64
import astroid
75
import astroid.builder
@@ -10,14 +8,6 @@
108
from . import _astroid_utils
119

1210

13-
if sys.version_info < (3, 10): # PY310
14-
from stdlib_list import in_stdlib
15-
else:
16-
17-
def in_stdlib(module_name: str) -> bool:
18-
return module_name in sys.stdlib_module_names
19-
20-
2111
def _prepare_docstring(doc):
2212
return "\n".join(sphinx.util.docstrings.prepare_docstring(doc))
2313

@@ -117,55 +107,69 @@ def _parse_assign(self, node):
117107

118108
return [data]
119109

120-
def parse_classdef(self, node, data=None):
110+
def _parse_classdef(self, node, use_name_stacks):
111+
if use_name_stacks:
112+
qual_name = self._get_qual_name(node.name)
113+
full_name = self._get_full_name(node.name)
114+
115+
self._qual_name_stack.append(node.name)
116+
self._full_name_stack.append(node.name)
117+
else:
118+
qual_name = node.qname()[len(node.root().qname()) + 1 :]
119+
full_name = node.qname()
120+
121121
type_ = "class"
122122
if _astroid_utils.is_exception(node):
123123
type_ = "exception"
124124

125-
basenames = list(_astroid_utils.get_full_basenames(node))
126-
127125
data = {
128126
"type": type_,
129127
"name": node.name,
130-
"qual_name": self._get_qual_name(node.name),
131-
"full_name": self._get_full_name(node.name),
132-
"bases": basenames,
128+
"qual_name": qual_name,
129+
"full_name": full_name,
130+
"bases": list(_astroid_utils.get_full_basenames(node)),
133131
"doc": _prepare_docstring(_astroid_utils.get_class_docstring(node)),
134132
"from_line_no": node.fromlineno,
135133
"to_line_no": node.tolineno,
136134
"children": [],
135+
"is_abstract": _astroid_utils.is_abstract_class(node),
137136
}
138137

139-
self._qual_name_stack.append(node.name)
140-
self._full_name_stack.append(node.name)
141-
overridden = set()
142138
overloads = {}
139+
for child in node.get_children():
140+
children_data = self.parse(child)
141+
for child_data in children_data:
142+
if _parse_child(child_data, overloads):
143+
data["children"].append(child_data)
144+
145+
data["children"] = list(self._resolve_inheritance(data))
146+
147+
return data
148+
149+
def _resolve_inheritance(self, *mro_data):
150+
overridden = set()
143151
children = {}
144-
for base in itertools.chain(iter((node,)), node.ancestors()):
152+
for i, cls_data in enumerate(mro_data):
145153
seen = set()
146154
base_children = []
155+
overloads = {}
147156

148-
# Don't document members inherited from standard library classes
149-
# unless that class is abstract.
150-
base_module = base.qname().split(".", 1)[0]
151-
if in_stdlib(base_module) and not _astroid_utils.is_abstract_class(base):
152-
continue
153-
154-
for child in base.get_children():
155-
children_data = self.parse(child)
156-
for child_data in children_data:
157-
name = child_data["name"]
157+
for child_data in cls_data["children"]:
158+
name = child_data["name"]
158159

159-
existing_child = children.get(name)
160-
if existing_child and not existing_child["doc"]:
161-
existing_child["doc"] = child_data["doc"]
160+
existing_child = children.get(name)
161+
if existing_child and not existing_child["doc"]:
162+
existing_child["doc"] = child_data["doc"]
162163

163-
if name in overridden:
164-
continue
164+
if name in overridden:
165+
continue
165166

166-
seen.add(name)
167-
if _parse_child(node, child_data, overloads, base):
168-
base_children.append(child_data)
167+
seen.add(name)
168+
if _parse_child(child_data, overloads):
169+
base_children.append(child_data)
170+
child_data["inherited"] = i != 0
171+
if child_data["inherited"]:
172+
child_data["inherited_from"] = cls_data
169173

170174
overridden.update(seen)
171175

@@ -185,7 +189,29 @@ def parse_classdef(self, node, data=None):
185189

186190
children[base_child["name"]] = base_child
187191

188-
data["children"].extend(children.values())
192+
return children.values()
193+
194+
def _relevant_ancestors(self, node):
195+
for base in node.ancestors():
196+
if base.qname() in (
197+
"__builtins__.object",
198+
"builtins.object",
199+
"builtins.type",
200+
):
201+
continue
202+
203+
yield base
204+
205+
def parse_classdef(self, node):
206+
data = self._parse_classdef(node, use_name_stacks=True)
207+
208+
ancestors = self._relevant_ancestors(node)
209+
ancestor_data = [
210+
self._parse_classdef(base, use_name_stacks=False) for base in ancestors
211+
]
212+
if ancestor_data:
213+
data["children"] = list(self._resolve_inheritance(data, *ancestor_data))
214+
189215
self._qual_name_stack.pop()
190216
self._full_name_stack.pop()
191217

@@ -227,7 +253,7 @@ def parse_functiondef(self, node):
227253
"qual_name": self._get_qual_name(node.name),
228254
"full_name": self._get_full_name(node.name),
229255
"args": _astroid_utils.get_args_info(node.args),
230-
"doc": _prepare_docstring(_astroid_utils.get_func_docstring(node)),
256+
"doc": _prepare_docstring(node.doc_node.value if node.doc_node else ""),
231257
"from_line_no": node.fromlineno,
232258
"to_line_no": node.tolineno,
233259
"return_annotation": _astroid_utils.get_return_annotation(node),
@@ -302,7 +328,7 @@ def parse_module(self, node):
302328
children_data = self.parse(child)
303329

304330
for child_data in children_data:
305-
if _parse_child(node, child_data, overloads):
331+
if _parse_child(child_data, overloads):
306332
data["children"].append(child_data)
307333

308334
return data
@@ -352,7 +378,7 @@ def parse(self, node):
352378
return data
353379

354380

355-
def _parse_child(node, child_data, overloads, base=None) -> bool:
381+
def _parse_child(child_data, overloads) -> bool:
356382
if child_data["type"] in ("function", "method", "property"):
357383
name = child_data["name"]
358384
if name in overloads:
@@ -369,7 +395,4 @@ def _parse_child(node, child_data, overloads, base=None) -> bool:
369395
if child_data["is_overload"] and name not in overloads:
370396
overloads[name] = child_data
371397

372-
if base:
373-
child_data["inherited"] = base is not node
374-
375398
return True

docs/changes/478.bugfix.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix all class members missing when documenting a module with the same name as a standard library module
2+
3+
Members inherited from the standard library can also have their skip value
4+
overridden by autoapi-skip-member.

tests/python/conftest.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import io
21
import os
32
import pathlib
43
import shutil
5-
from unittest.mock import call
64

75
from bs4 import BeautifulSoup
86
import pytest

tests/python/pystdlib/conf.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
templates_path = ["_templates"]
2+
source_suffix = ".rst"
3+
master_doc = "index"
4+
project = "pystdlib"
5+
copyright = "2015, readthedocs"
6+
author = "readthedocs"
7+
version = "0.1"
8+
release = "0.1"
9+
language = "en"
10+
exclude_patterns = ["_build"]
11+
pygments_style = "sphinx"
12+
todo_include_todos = False
13+
html_theme = "alabaster"
14+
htmlhelp_basename = "pystdlibdoc"
15+
extensions = ["autoapi.extension"]
16+
autoapi_dirs = ["stdlib"]
17+
autoapi_file_pattern = "*.py"
18+
autoapi_keep_files = True
19+
autoapi_options = [
20+
"members",
21+
"undoc-members" "special-members",
22+
"imported-members",
23+
"inherited-members",
24+
]

tests/python/pystdlib/index.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
.. pypackageexample documentation master file, created by
2+
sphinx-quickstart on Fri May 29 13:34:37 2015.
3+
You can adapt this file completely to your liking, but it should at least
4+
contain the root `toctree` directive.
5+
6+
Welcome to pypackageexample's documentation!
7+
============================================
8+
9+
.. toctree::
10+
11+
autoapi/index
12+
13+
Contents:
14+
15+
.. toctree::
16+
:maxdepth: 2
17+
18+
19+
20+
Indices and tables
21+
==================
22+
23+
* :ref:`genindex`
24+
* :ref:`modindex`
25+
* :ref:`search`
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""This is a docstring."""
2+
3+
import ast
4+
5+
6+
class MyVisitor(ast.NodeVisitor):
7+
"""My custom visitor."""
8+
9+
def my_visit(self):
10+
"""My visit method."""
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""This is a docstring."""
2+
3+
from ssl import SSLContext
4+
5+
6+
class MySSLContext(SSLContext):
7+
"""This is a class."""
8+
9+
def my_method(self):
10+
"""This is a method."""
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""ssl is the same name as a stdlib module."""
2+
3+
4+
class SSLContext:
5+
"""Do things with ssl."""
6+
7+
def wrap_socket(self, sock):
8+
"""Wrap a socket."""
9+
return sock

0 commit comments

Comments
 (0)