This repository provides User Manual for setting up a Docker environment tailored for testing DGTD code.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

408 lines
16 KiB

"""Importer utilities for autodoc"""
from __future__ import annotations
import contextlib
import importlib
import os
import sys
import traceback
import typing
from enum import Enum
from typing import TYPE_CHECKING, NamedTuple
from sphinx.ext.autodoc.mock import ismock, undecorate
from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import logging
from sphinx.util.inspect import (
getannotations,
getmro,
getslots,
isclass,
isenumclass,
safe_getattr,
unwrap_all,
)
if TYPE_CHECKING:
from collections.abc import Callable, Iterator, Mapping
from types import ModuleType
from typing import Any
from sphinx.ext.autodoc import ObjectMember
logger = logging.getLogger(__name__)
def _filter_enum_dict(
enum_class: type[Enum],
attrgetter: Callable[[Any, str, Any], Any],
enum_class_dict: Mapping[str, object],
) -> Iterator[tuple[str, type, Any]]:
"""Find the attributes to document of an enumeration class.
The output consists of triplets ``(attribute name, defining class, value)``
where the attribute name can appear more than once during the iteration
but with different defining class. The order of occurrence is guided by
the MRO of *enum_class*.
"""
# attributes that were found on a mixin type or the data type
candidate_in_mro: set[str] = set()
# sunder names that were picked up (and thereby allowed to be redefined)
# see: https://docs.python.org/3/howto/enum.html#supported-dunder-names
sunder_names = {'_name_', '_value_', '_missing_', '_order_', '_generate_next_value_'}
# attributes that can be picked up on a mixin type or the enum's data type
public_names = {'name', 'value', *object.__dict__, *sunder_names}
# names that are ignored by default
ignore_names = Enum.__dict__.keys() - public_names
def is_native_api(obj: object, name: str) -> bool:
"""Check whether *obj* is the same as ``Enum.__dict__[name]``."""
return unwrap_all(obj) is unwrap_all(Enum.__dict__[name])
def should_ignore(name: str, value: Any) -> bool:
if name in sunder_names:
return is_native_api(value, name)
return name in ignore_names
sentinel = object()
def query(name: str, defining_class: type) -> tuple[str, type, Any] | None:
value = attrgetter(enum_class, name, sentinel)
if value is not sentinel:
return (name, defining_class, value)
return None
# attributes defined on a parent type, possibly shadowed later by
# the attributes defined directly inside the enumeration class
for parent in enum_class.__mro__:
if parent in {enum_class, Enum, object}:
continue
parent_dict = attrgetter(parent, '__dict__', {})
for name, value in parent_dict.items():
if should_ignore(name, value):
continue
candidate_in_mro.add(name)
if (item := query(name, parent)) is not None:
yield item
# exclude members coming from the native Enum unless
# they were redefined on a mixin type or the data type
excluded_members = Enum.__dict__.keys() - candidate_in_mro
yield from filter(None, (query(name, enum_class) for name in enum_class_dict
if name not in excluded_members))
# check if allowed members from ``Enum`` were redefined at the enum level
special_names = sunder_names | public_names
special_names &= enum_class_dict.keys()
special_names &= Enum.__dict__.keys()
for name in special_names:
if (
not is_native_api(enum_class_dict[name], name)
and (item := query(name, enum_class)) is not None
):
yield item
def mangle(subject: Any, name: str) -> str:
"""Mangle the given name."""
try:
if isclass(subject) and name.startswith('__') and not name.endswith('__'):
return f"_{subject.__name__}{name}"
except AttributeError:
pass
return name
def unmangle(subject: Any, name: str) -> str | None:
"""Unmangle the given name."""
try:
if isclass(subject) and not name.endswith('__'):
prefix = "_%s__" % subject.__name__
if name.startswith(prefix):
return name.replace(prefix, "__", 1)
else:
for cls in subject.__mro__:
prefix = "_%s__" % cls.__name__
if name.startswith(prefix):
# mangled attribute defined in parent class
return None
except AttributeError:
pass
return name
def import_module(modname: str, warningiserror: bool = False) -> Any:
"""Call importlib.import_module(modname), convert exceptions to ImportError."""
try:
with logging.skip_warningiserror(not warningiserror):
return importlib.import_module(modname)
except BaseException as exc:
# Importing modules may cause any side effects, including
# SystemExit, so we need to catch all errors.
raise ImportError(exc, traceback.format_exc()) from exc
def _reload_module(module: ModuleType, warningiserror: bool = False) -> Any:
"""
Call importlib.reload(module), convert exceptions to ImportError
"""
try:
with logging.skip_warningiserror(not warningiserror):
return importlib.reload(module)
except BaseException as exc:
# Importing modules may cause any side effects, including
# SystemExit, so we need to catch all errors.
raise ImportError(exc, traceback.format_exc()) from exc
def import_object(modname: str, objpath: list[str], objtype: str = '',
attrgetter: Callable[[Any, str], Any] = safe_getattr,
warningiserror: bool = False) -> Any:
if objpath:
logger.debug('[autodoc] from %s import %s', modname, '.'.join(objpath))
else:
logger.debug('[autodoc] import %s', modname)
try:
module = None
exc_on_importing = None
objpath = objpath.copy()
while module is None:
try:
original_module_names = frozenset(sys.modules)
module = import_module(modname, warningiserror=warningiserror)
if os.environ.get('SPHINX_AUTODOC_RELOAD_MODULES'):
new_modules = [m for m in sys.modules if m not in original_module_names]
# Try reloading modules with ``typing.TYPE_CHECKING == True``.
try:
typing.TYPE_CHECKING = True
# Ignore failures; we've already successfully loaded these modules
with contextlib.suppress(ImportError, KeyError):
for m in new_modules:
_reload_module(sys.modules[m])
finally:
typing.TYPE_CHECKING = False
module = sys.modules[modname]
logger.debug('[autodoc] import %s => %r', modname, module)
except ImportError as exc:
logger.debug('[autodoc] import %s => failed', modname)
exc_on_importing = exc
if '.' in modname:
# retry with parent module
modname, name = modname.rsplit('.', 1)
objpath.insert(0, name)
else:
raise
obj = module
parent = None
object_name = None
for attrname in objpath:
parent = obj
logger.debug('[autodoc] getattr(_, %r)', attrname)
mangled_name = mangle(obj, attrname)
obj = attrgetter(obj, mangled_name)
try:
logger.debug('[autodoc] => %r', obj)
except TypeError:
# fallback of failure on logging for broken object
# refs: https://github.com/sphinx-doc/sphinx/issues/9095
logger.debug('[autodoc] => %r', (obj,))
object_name = attrname
return [module, parent, object_name, obj]
except (AttributeError, ImportError) as exc:
if isinstance(exc, AttributeError) and exc_on_importing:
# restore ImportError
exc = exc_on_importing
if objpath:
errmsg = ('autodoc: failed to import %s %r from module %r' %
(objtype, '.'.join(objpath), modname))
else:
errmsg = f'autodoc: failed to import {objtype} {modname!r}'
if isinstance(exc, ImportError):
# import_module() raises ImportError having real exception obj and
# traceback
real_exc, traceback_msg = exc.args
if isinstance(real_exc, SystemExit):
errmsg += ('; the module executes module level statement '
'and it might call sys.exit().')
elif isinstance(real_exc, ImportError) and real_exc.args:
errmsg += '; the following exception was raised:\n%s' % real_exc.args[0]
else:
errmsg += '; the following exception was raised:\n%s' % traceback_msg
else:
errmsg += '; the following exception was raised:\n%s' % traceback.format_exc()
logger.debug(errmsg)
raise ImportError(errmsg) from exc
class Attribute(NamedTuple):
name: str
directly_defined: bool
value: Any
def get_object_members(
subject: Any,
objpath: list[str],
attrgetter: Callable,
analyzer: ModuleAnalyzer | None = None,
) -> dict[str, Attribute]:
"""Get members and attributes of target object."""
from sphinx.ext.autodoc import INSTANCEATTR
# the members directly defined in the class
obj_dict = attrgetter(subject, '__dict__', {})
members: dict[str, Attribute] = {}
# enum members
if isenumclass(subject):
for name, defining_class, value in _filter_enum_dict(subject, attrgetter, obj_dict):
# the order of occurrence of *name* matches the subject's MRO,
# allowing inherited attributes to be shadowed correctly
if unmangled := unmangle(defining_class, name):
members[unmangled] = Attribute(unmangled, defining_class is subject, value)
# members in __slots__
try:
subject___slots__ = getslots(subject)
if subject___slots__:
from sphinx.ext.autodoc import SLOTSATTR
for name in subject___slots__:
members[name] = Attribute(name, True, SLOTSATTR)
except (TypeError, ValueError):
pass
# other members
for name in dir(subject):
try:
value = attrgetter(subject, name)
directly_defined = name in obj_dict
unmangled = unmangle(subject, name)
if unmangled and unmangled not in members:
members[unmangled] = Attribute(unmangled, directly_defined, value)
except AttributeError:
continue
# annotation only member (ex. attr: int)
for cls in getmro(subject):
for name in getannotations(cls):
unmangled = unmangle(cls, name)
if unmangled and unmangled not in members:
members[unmangled] = Attribute(unmangled, cls is subject, INSTANCEATTR)
if analyzer:
# append instance attributes (cf. self.attr1) if analyzer knows
namespace = '.'.join(objpath)
for (ns, name) in analyzer.find_attr_docs():
if namespace == ns and name not in members:
members[name] = Attribute(name, True, INSTANCEATTR)
return members
def get_class_members(subject: Any, objpath: Any, attrgetter: Callable,
inherit_docstrings: bool = True) -> dict[str, ObjectMember]:
"""Get members and attributes of target class."""
from sphinx.ext.autodoc import INSTANCEATTR, ObjectMember
# the members directly defined in the class
obj_dict = attrgetter(subject, '__dict__', {})
members: dict[str, ObjectMember] = {}
# enum members
if isenumclass(subject):
for name, defining_class, value in _filter_enum_dict(subject, attrgetter, obj_dict):
# the order of occurrence of *name* matches the subject's MRO,
# allowing inherited attributes to be shadowed correctly
if unmangled := unmangle(defining_class, name):
members[unmangled] = ObjectMember(unmangled, value, class_=defining_class)
# members in __slots__
try:
subject___slots__ = getslots(subject)
if subject___slots__:
from sphinx.ext.autodoc import SLOTSATTR
for name, docstring in subject___slots__.items():
members[name] = ObjectMember(name, SLOTSATTR, class_=subject,
docstring=docstring)
except (TypeError, ValueError):
pass
# other members
for name in dir(subject):
try:
value = attrgetter(subject, name)
if ismock(value):
value = undecorate(value)
unmangled = unmangle(subject, name)
if unmangled and unmangled not in members:
if name in obj_dict:
members[unmangled] = ObjectMember(unmangled, value, class_=subject)
else:
members[unmangled] = ObjectMember(unmangled, value)
except AttributeError:
continue
try:
for cls in getmro(subject):
try:
modname = safe_getattr(cls, '__module__')
qualname = safe_getattr(cls, '__qualname__')
analyzer = ModuleAnalyzer.for_module(modname)
analyzer.analyze()
except AttributeError:
qualname = None
analyzer = None
except PycodeError:
analyzer = None
# annotation only member (ex. attr: int)
for name in getannotations(cls):
unmangled = unmangle(cls, name)
if unmangled and unmangled not in members:
if analyzer and (qualname, unmangled) in analyzer.attr_docs:
docstring = '\n'.join(analyzer.attr_docs[qualname, unmangled])
else:
docstring = None
members[unmangled] = ObjectMember(unmangled, INSTANCEATTR, class_=cls,
docstring=docstring)
# append or complete instance attributes (cf. self.attr1) if analyzer knows
if analyzer:
for (ns, name), docstring in analyzer.attr_docs.items():
if ns == qualname and name not in members:
# otherwise unknown instance attribute
members[name] = ObjectMember(name, INSTANCEATTR, class_=cls,
docstring='\n'.join(docstring))
elif (ns == qualname and docstring and
isinstance(members[name], ObjectMember) and
not members[name].docstring):
if cls != subject and not inherit_docstrings:
# If we are in the MRO of the class and not the class itself,
# and we do not want to inherit docstrings, then skip setting
# the docstring below
continue
# attribute is already known, because dir(subject) enumerates it.
# But it has no docstring yet
members[name].docstring = '\n'.join(docstring)
except AttributeError:
pass
return members