Skip to content

Commit

Permalink
Fix all class members missing when documenting a module with the same…
Browse files Browse the repository at this point in the history
… name as a standard library module

Fixes #478
  • Loading branch information
AWhetter committed Sep 2, 2024
1 parent 74770d3 commit 5d53f92
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 78 deletions.
30 changes: 0 additions & 30 deletions autoapi/_astroid_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,36 +644,6 @@ def get_return_annotation(node: astroid.nodes.FunctionDef) -> str | None:
return return_annotation


def get_func_docstring(node: astroid.nodes.FunctionDef) -> str:
"""Get the docstring of a node, using a parent docstring if needed.
Args:
node: The node to get a docstring for.
Returns:
The docstring of the function, or the empty string if no docstring
was found or defined.
"""
doc = node.doc_node.value if node.doc_node else ""

if not doc and isinstance(node.parent, astroid.nodes.ClassDef):
for base in node.parent.ancestors():
if node.name in ("__init__", "__new__"):
base_module = base.qname().split(".", 1)[0]
if in_stdlib(base_module):
continue

for child in base.get_children():
if (
isinstance(child, node.__class__)
and child.name == node.name
and child.doc_node is not None
):
return child.doc_node.value

return doc


def get_class_docstring(node: astroid.nodes.ClassDef) -> str:
"""Get the docstring of a node, using a parent docstring if needed.
Expand Down
28 changes: 28 additions & 0 deletions autoapi/_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import operator
import os
import re
import sys

from jinja2 import Environment, FileSystemLoader
import sphinx
Expand All @@ -31,6 +32,14 @@
)
from .settings import OWN_PAGE_LEVELS, TEMPLATE_DIR

if sys.version_info < (3, 10): # PY310
from stdlib_list import in_stdlib
else:

def in_stdlib(module_name: str) -> bool:
return module_name in sys.stdlib_module_names


LOGGER = sphinx.util.logging.getLogger(__name__)


Expand Down Expand Up @@ -451,6 +460,24 @@ def read_file(self, path, **kwargs):
)
return None

def _skip_if_stdlib(self):
documented_modules = {obj["full_name"] for obj in self.paths.values()}

q = collections.deque(self.paths.values())
while q:
obj = q.popleft()
if "children" in obj:
q.extend(obj["children"])

if obj.get("inherited", False):
module = obj["inherited_from"]["full_name"].split(".", 1)[0]
if (
in_stdlib(module)
and not obj["inherited_from"]["is_abstract"]
and module not in documented_modules
):
obj["hide"] = True

def _resolve_placeholders(self):
"""Resolve objects that have been imported from elsewhere."""
modules = {}
Expand All @@ -477,6 +504,7 @@ def _hide_yo_kids(self):
child["hide"] = True

def map(self, options=None):
self._skip_if_stdlib()
self._resolve_placeholders()
self._hide_yo_kids()
self.app.env.autoapi_annotations = {}
Expand Down
113 changes: 68 additions & 45 deletions autoapi/_parser.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import collections
import itertools
import os
import sys

import astroid
import astroid.builder
Expand All @@ -10,14 +8,6 @@
from . import _astroid_utils


if sys.version_info < (3, 10): # PY310
from stdlib_list import in_stdlib
else:

def in_stdlib(module_name: str) -> bool:
return module_name in sys.stdlib_module_names


def _prepare_docstring(doc):
return "\n".join(sphinx.util.docstrings.prepare_docstring(doc))

Expand Down Expand Up @@ -117,55 +107,69 @@ def _parse_assign(self, node):

return [data]

def parse_classdef(self, node, data=None):
def _parse_classdef(self, node, use_name_stacks):
if use_name_stacks:
qual_name = self._get_qual_name(node.name)
full_name = self._get_full_name(node.name)

self._qual_name_stack.append(node.name)
self._full_name_stack.append(node.name)
else:
qual_name = node.qname()[len(node.root().qname()) + 1 :]
full_name = node.qname()

type_ = "class"
if _astroid_utils.is_exception(node):
type_ = "exception"

basenames = list(_astroid_utils.get_full_basenames(node))

data = {
"type": type_,
"name": node.name,
"qual_name": self._get_qual_name(node.name),
"full_name": self._get_full_name(node.name),
"bases": basenames,
"qual_name": qual_name,
"full_name": full_name,
"bases": list(_astroid_utils.get_full_basenames(node)),
"doc": _prepare_docstring(_astroid_utils.get_class_docstring(node)),
"from_line_no": node.fromlineno,
"to_line_no": node.tolineno,
"children": [],
"is_abstract": _astroid_utils.is_abstract_class(node),
}

self._qual_name_stack.append(node.name)
self._full_name_stack.append(node.name)
overridden = set()
overloads = {}
for child in node.get_children():
children_data = self.parse(child)
for child_data in children_data:
if _parse_child(child_data, overloads):
data["children"].append(child_data)

data["children"] = list(self._resolve_inheritance(data))

return data

def _resolve_inheritance(self, *mro_data):
overridden = set()
children = {}
for base in itertools.chain(iter((node,)), node.ancestors()):
for i, cls_data in enumerate(mro_data):
seen = set()
base_children = []
overloads = {}

# Don't document members inherited from standard library classes
# unless that class is abstract.
base_module = base.qname().split(".", 1)[0]
if in_stdlib(base_module) and not _astroid_utils.is_abstract_class(base):
continue

for child in base.get_children():
children_data = self.parse(child)
for child_data in children_data:
name = child_data["name"]
for child_data in cls_data["children"]:
name = child_data["name"]

existing_child = children.get(name)
if existing_child and not existing_child["doc"]:
existing_child["doc"] = child_data["doc"]
existing_child = children.get(name)
if existing_child and not existing_child["doc"]:
existing_child["doc"] = child_data["doc"]

if name in overridden:
continue
if name in overridden:
continue

seen.add(name)
if _parse_child(node, child_data, overloads, base):
base_children.append(child_data)
seen.add(name)
if _parse_child(child_data, overloads):
base_children.append(child_data)
child_data["inherited"] = i != 0
if child_data["inherited"]:
child_data["inherited_from"] = cls_data

overridden.update(seen)

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

children[base_child["name"]] = base_child

data["children"].extend(children.values())
return children.values()

def _relevant_ancestors(self, node):
for base in node.ancestors():
if base.qname() in (
"__builtins__.object",
"builtins.object",
"builtins.type",
):
continue

yield base

def parse_classdef(self, node):
data = self._parse_classdef(node, use_name_stacks=True)

ancestors = self._relevant_ancestors(node)
ancestor_data = [
self._parse_classdef(base, use_name_stacks=False) for base in ancestors
]
if ancestor_data:
data["children"] = list(self._resolve_inheritance(data, *ancestor_data))

self._qual_name_stack.pop()
self._full_name_stack.pop()

Expand Down Expand Up @@ -227,7 +253,7 @@ def parse_functiondef(self, node):
"qual_name": self._get_qual_name(node.name),
"full_name": self._get_full_name(node.name),
"args": _astroid_utils.get_args_info(node.args),
"doc": _prepare_docstring(_astroid_utils.get_func_docstring(node)),
"doc": _prepare_docstring(node.doc_node.value if node.doc_node else ""),
"from_line_no": node.fromlineno,
"to_line_no": node.tolineno,
"return_annotation": _astroid_utils.get_return_annotation(node),
Expand Down Expand Up @@ -302,7 +328,7 @@ def parse_module(self, node):
children_data = self.parse(child)

for child_data in children_data:
if _parse_child(node, child_data, overloads):
if _parse_child(child_data, overloads):
data["children"].append(child_data)

return data
Expand Down Expand Up @@ -352,7 +378,7 @@ def parse(self, node):
return data


def _parse_child(node, child_data, overloads, base=None) -> bool:
def _parse_child(child_data, overloads) -> bool:
if child_data["type"] in ("function", "method", "property"):
name = child_data["name"]
if name in overloads:
Expand All @@ -369,7 +395,4 @@ def _parse_child(node, child_data, overloads, base=None) -> bool:
if child_data["is_overload"] and name not in overloads:
overloads[name] = child_data

if base:
child_data["inherited"] = base is not node

return True
4 changes: 4 additions & 0 deletions docs/changes/478.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix all class members missing when documenting a module with the same name as a standard library module

Members inherited from the standard library can also have their skip value
overridden by autoapi-skip-member.
2 changes: 0 additions & 2 deletions tests/python/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import io
import os
import pathlib
import shutil
from unittest.mock import call

from bs4 import BeautifulSoup
import pytest
Expand Down
24 changes: 24 additions & 0 deletions tests/python/pystdlib/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
templates_path = ["_templates"]
source_suffix = ".rst"
master_doc = "index"
project = "pystdlib"
copyright = "2015, readthedocs"
author = "readthedocs"
version = "0.1"
release = "0.1"
language = "en"
exclude_patterns = ["_build"]
pygments_style = "sphinx"
todo_include_todos = False
html_theme = "alabaster"
htmlhelp_basename = "pystdlibdoc"
extensions = ["autoapi.extension"]
autoapi_dirs = ["stdlib"]
autoapi_file_pattern = "*.py"
autoapi_keep_files = True
autoapi_options = [
"members",
"undoc-members" "special-members",
"imported-members",
"inherited-members",
]
25 changes: 25 additions & 0 deletions tests/python/pystdlib/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.. pypackageexample documentation master file, created by
sphinx-quickstart on Fri May 29 13:34:37 2015.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to pypackageexample's documentation!
============================================

.. toctree::

autoapi/index

Contents:

.. toctree::
:maxdepth: 2



Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
10 changes: 10 additions & 0 deletions tests/python/pystdlib/stdlib/myast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""This is a docstring."""

import ast


class MyVisitor(ast.NodeVisitor):
"""My custom visitor."""

def my_visit(self):
"""My visit method."""
10 changes: 10 additions & 0 deletions tests/python/pystdlib/stdlib/myssl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""This is a docstring."""

from ssl import SSLContext


class MySSLContext(SSLContext):
"""This is a class."""

def my_method(self):
"""This is a method."""
9 changes: 9 additions & 0 deletions tests/python/pystdlib/stdlib/ssl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""ssl is the same name as a stdlib module."""


class SSLContext:
"""Do things with ssl."""

def wrap_socket(self, sock):
"""Wrap a socket."""
return sock
Loading

0 comments on commit 5d53f92

Please sign in to comment.