diff options
Diffstat (limited to 'sphinx/domains/python.py')
-rw-r--r-- | sphinx/domains/python.py | 330 |
1 files changed, 151 insertions, 179 deletions
diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 552742bb8..d0c5f7118 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -15,17 +15,17 @@ import sys import typing import warnings from inspect import Parameter -from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Tuple, cast +from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Tuple, Type, cast from docutils import nodes from docutils.nodes import Element, Node from docutils.parsers.rst import directives from sphinx import addnodes -from sphinx.addnodes import desc_signature, pending_xref +from sphinx.addnodes import desc_signature, pending_xref, pending_xref_condition from sphinx.application import Sphinx from sphinx.builders import Builder -from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning +from sphinx.deprecation import RemovedInSphinx50Warning from sphinx.directives import ObjectDescription from sphinx.domains import Domain, Index, IndexEntry, ObjType from sphinx.environment import BuildEnvironment @@ -37,13 +37,8 @@ from sphinx.util import logging from sphinx.util.docfields import Field, GroupedField, TypedField from sphinx.util.docutils import SphinxDirective from sphinx.util.inspect import signature_from_str -from sphinx.util.nodes import make_id, make_refnode -from sphinx.util.typing import TextlikeNode - -if False: - # For type annotation - from typing import Type # for python3.5.1 - +from sphinx.util.nodes import find_pending_xref_condition, make_id, make_refnode +from sphinx.util.typing import OptionSpec, TextlikeNode logger = logging.getLogger(__name__) @@ -68,14 +63,20 @@ pairindextypes = { 'builtin': _('built-in function'), } -ObjectEntry = NamedTuple('ObjectEntry', [('docname', str), - ('node_id', str), - ('objtype', str)]) -ModuleEntry = NamedTuple('ModuleEntry', [('docname', str), - ('node_id', str), - ('synopsis', str), - ('platform', str), - ('deprecated', bool)]) + +class ObjectEntry(NamedTuple): + docname: str + node_id: str + objtype: str + canonical: bool + + +class ModuleEntry(NamedTuple): + docname: str + node_id: str + synopsis: str + platform: str + deprecated: bool def type_to_xref(text: str, env: BuildEnvironment = None) -> addnodes.pending_xref: @@ -91,7 +92,17 @@ def type_to_xref(text: str, env: BuildEnvironment = None) -> addnodes.pending_xr else: kwargs = {} - return pending_xref('', nodes.Text(text), + if env.config.python_use_unqualified_type_names: + # Note: It would be better to use qualname to describe the object to support support + # nested classes. But python domain can't access the real python object because this + # module should work not-dynamically. + shortname = text.split('.')[-1] + contnodes: List[Node] = [pending_xref_condition('', shortname, condition='resolved'), + pending_xref_condition('', text, condition='*')] + else: + contnodes = [nodes.Text(text)] + + return pending_xref('', *contnodes, refdomain='py', reftype=reftype, reftarget=text, **kwargs) @@ -101,12 +112,17 @@ def _parse_annotation(annotation: str, env: BuildEnvironment = None) -> List[Nod if isinstance(node, ast.Attribute): return [nodes.Text("%s.%s" % (unparse(node.value)[0], node.attr))] elif isinstance(node, ast.BinOp): - result = unparse(node.left) # type: List[Node] + result: List[Node] = unparse(node.left) result.extend(unparse(node.op)) result.extend(unparse(node.right)) return result elif isinstance(node, ast.BitOr): return [nodes.Text(' '), addnodes.desc_sig_punctuation('', '|'), nodes.Text(' ')] + elif isinstance(node, ast.Constant): # type: ignore + if node.value is Ellipsis: + return [addnodes.desc_sig_punctuation('', "...")] + else: + return [nodes.Text(node.value)] elif isinstance(node, ast.Expr): return unparse(node.value) elif isinstance(node, ast.Index): @@ -142,13 +158,6 @@ def _parse_annotation(annotation: str, env: BuildEnvironment = None) -> List[Nod return result else: - if sys.version_info >= (3, 6): - if isinstance(node, ast.Constant): - if node.value is Ellipsis: - return [addnodes.desc_sig_punctuation('', "...")] - else: - return [nodes.Text(node.value)] - if sys.version_info < (3, 8): if isinstance(node, ast.Ellipsis): return [addnodes.desc_sig_punctuation('', "...")] @@ -230,7 +239,7 @@ def _pseudo_parse_arglist(signode: desc_signature, arglist: str) -> None: string literal (e.g. default argument value). """ paramlist = addnodes.desc_parameterlist() - stack = [paramlist] # type: List[Element] + stack: List[Element] = [paramlist] try: for argument in arglist.split(','): argument = argument.strip() @@ -274,7 +283,7 @@ def _pseudo_parse_arglist(signode: desc_signature, arglist: str) -> None: # when it comes to handling "." and "~" prefixes. class PyXrefMixin: def make_xref(self, rolename: str, domain: str, target: str, - innernode: "Type[TextlikeNode]" = nodes.emphasis, + innernode: Type[TextlikeNode] = nodes.emphasis, contnode: Node = None, env: BuildEnvironment = None) -> Node: result = super().make_xref(rolename, domain, target, # type: ignore innernode, contnode, env) @@ -293,7 +302,7 @@ class PyXrefMixin: return result def make_xrefs(self, rolename: str, domain: str, target: str, - innernode: "Type[TextlikeNode]" = nodes.emphasis, + innernode: Type[TextlikeNode] = nodes.emphasis, contnode: Node = None, env: BuildEnvironment = None) -> List[Node]: delims = r'(\s*[\[\]\(\),](?:\s*or\s)?\s*|\s+or\s+)' delims_re = re.compile(delims) @@ -317,7 +326,7 @@ class PyXrefMixin: class PyField(PyXrefMixin, Field): def make_xref(self, rolename: str, domain: str, target: str, - innernode: "Type[TextlikeNode]" = nodes.emphasis, + innernode: Type[TextlikeNode] = nodes.emphasis, contnode: Node = None, env: BuildEnvironment = None) -> Node: if rolename == 'class' and target == 'None': # None is not a type, so use obj role instead. @@ -332,7 +341,7 @@ class PyGroupedField(PyXrefMixin, GroupedField): class PyTypedField(PyXrefMixin, TypedField): def make_xref(self, rolename: str, domain: str, target: str, - innernode: "Type[TextlikeNode]" = nodes.emphasis, + innernode: Type[TextlikeNode] = nodes.emphasis, contnode: Node = None, env: BuildEnvironment = None) -> Node: if rolename == 'class' and target == 'None': # None is not a type, so use obj role instead. @@ -348,10 +357,11 @@ class PyObject(ObjectDescription[Tuple[str, str]]): :cvar allow_nesting: Class is an object that allows for nested namespaces :vartype allow_nesting: bool """ - option_spec = { + option_spec: OptionSpec = { 'noindex': directives.flag, 'noindexentry': directives.flag, 'module': directives.unchanged, + 'canonical': directives.unchanged, 'annotation': directives.unchanged, } @@ -361,7 +371,7 @@ class PyObject(ObjectDescription[Tuple[str, str]]): 'keyword', 'kwarg', 'kwparam'), typerolename='class', typenames=('paramtype', 'type'), can_collapse=True), - PyTypedField('variable', label=_('Variables'), rolename='obj', + PyTypedField('variable', label=_('Variables'), names=('var', 'ivar', 'cvar'), typerolename='class', typenames=('vartype',), can_collapse=True), @@ -493,6 +503,11 @@ class PyObject(ObjectDescription[Tuple[str, str]]): domain = cast(PythonDomain, self.env.get_domain('py')) domain.note_object(fullname, self.objtype, node_id, location=signode) + canonical_name = self.options.get('canonical') + if canonical_name: + domain.note_object(canonical_name, self.objtype, node_id, canonical=True, + location=signode) + if 'noindexentry' not in self.options: indextext = self.get_index_text(modname, name_cls) if indextext: @@ -557,44 +572,10 @@ class PyObject(ObjectDescription[Tuple[str, str]]): self.env.ref_context.pop('py:module') -class PyModulelevel(PyObject): - """ - Description of an object on module level (functions, data). - """ - - def run(self) -> List[Node]: - for cls in self.__class__.__mro__: - if cls.__name__ != 'DirectiveAdapter': - warnings.warn('PyModulelevel is deprecated. ' - 'Please check the implementation of %s' % cls, - RemovedInSphinx40Warning, stacklevel=2) - break - else: - warnings.warn('PyModulelevel is deprecated', - RemovedInSphinx40Warning, stacklevel=2) - - return super().run() - - def needs_arglist(self) -> bool: - return self.objtype == 'function' - - def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str: - if self.objtype == 'function': - if not modname: - return _('%s() (built-in function)') % name_cls[0] - return _('%s() (in module %s)') % (name_cls[0], modname) - elif self.objtype == 'data': - if not modname: - return _('%s (built-in variable)') % name_cls[0] - return _('%s (in module %s)') % (name_cls[0], modname) - else: - return '' - - class PyFunction(PyObject): """Description of a function.""" - option_spec = PyObject.option_spec.copy() + option_spec: OptionSpec = PyObject.option_spec.copy() option_spec.update({ 'async': directives.flag, }) @@ -648,7 +629,7 @@ class PyDecoratorFunction(PyFunction): class PyVariable(PyObject): """Description of a variable.""" - option_spec = PyObject.option_spec.copy() + option_spec: OptionSpec = PyObject.option_spec.copy() option_spec.update({ 'type': directives.unchanged, 'value': directives.unchanged, @@ -681,7 +662,7 @@ class PyClasslike(PyObject): Description of a class-like object (classes, interfaces, exceptions). """ - option_spec = PyObject.option_spec.copy() + option_spec: OptionSpec = PyObject.option_spec.copy() option_spec.update({ 'final': directives.flag, }) @@ -705,95 +686,10 @@ class PyClasslike(PyObject): return '' -class PyClassmember(PyObject): - """ - Description of a class member (methods, attributes). - """ - - def run(self) -> List[Node]: - for cls in self.__class__.__mro__: - if cls.__name__ != 'DirectiveAdapter': - warnings.warn('PyClassmember is deprecated. ' - 'Please check the implementation of %s' % cls, - RemovedInSphinx40Warning, stacklevel=2) - break - else: - warnings.warn('PyClassmember is deprecated', - RemovedInSphinx40Warning, stacklevel=2) - - return super().run() - - def needs_arglist(self) -> bool: - return self.objtype.endswith('method') - - def get_signature_prefix(self, sig: str) -> str: - if self.objtype == 'staticmethod': - return 'static ' - elif self.objtype == 'classmethod': - return 'classmethod ' - return '' - - def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str: - name, cls = name_cls - add_modules = self.env.config.add_module_names - if self.objtype == 'method': - try: - clsname, methname = name.rsplit('.', 1) - except ValueError: - if modname: - return _('%s() (in module %s)') % (name, modname) - else: - return '%s()' % name - if modname and add_modules: - return _('%s() (%s.%s method)') % (methname, modname, clsname) - else: - return _('%s() (%s method)') % (methname, clsname) - elif self.objtype == 'staticmethod': - try: - clsname, methname = name.rsplit('.', 1) - except ValueError: - if modname: - return _('%s() (in module %s)') % (name, modname) - else: - return '%s()' % name - if modname and add_modules: - return _('%s() (%s.%s static method)') % (methname, modname, - clsname) - else: - return _('%s() (%s static method)') % (methname, clsname) - elif self.objtype == 'classmethod': - try: - clsname, methname = name.rsplit('.', 1) - except ValueError: - if modname: - return _('%s() (in module %s)') % (name, modname) - else: - return '%s()' % name - if modname: - return _('%s() (%s.%s class method)') % (methname, modname, - clsname) - else: - return _('%s() (%s class method)') % (methname, clsname) - elif self.objtype == 'attribute': - try: - clsname, attrname = name.rsplit('.', 1) - except ValueError: - if modname: - return _('%s (in module %s)') % (name, modname) - else: - return name - if modname and add_modules: - return _('%s (%s.%s attribute)') % (attrname, modname, clsname) - else: - return _('%s (%s attribute)') % (attrname, clsname) - else: - return '' - - class PyMethod(PyObject): """Description of a method.""" - option_spec = PyObject.option_spec.copy() + option_spec: OptionSpec = PyObject.option_spec.copy() option_spec.update({ 'abstractmethod': directives.flag, 'async': directives.flag, @@ -854,7 +750,7 @@ class PyMethod(PyObject): class PyClassMethod(PyMethod): """Description of a classmethod.""" - option_spec = PyObject.option_spec.copy() + option_spec: OptionSpec = PyObject.option_spec.copy() def run(self) -> List[Node]: self.name = 'py:method' @@ -866,7 +762,7 @@ class PyClassMethod(PyMethod): class PyStaticMethod(PyMethod): """Description of a staticmethod.""" - option_spec = PyObject.option_spec.copy() + option_spec: OptionSpec = PyObject.option_spec.copy() def run(self) -> List[Node]: self.name = 'py:method' @@ -894,7 +790,7 @@ class PyDecoratorMethod(PyMethod): class PyAttribute(PyObject): """Description of an attribute.""" - option_spec = PyObject.option_spec.copy() + option_spec: OptionSpec = PyObject.option_spec.copy() option_spec.update({ 'type': directives.unchanged, 'value': directives.unchanged, @@ -929,6 +825,46 @@ class PyAttribute(PyObject): return _('%s (%s attribute)') % (attrname, clsname) +class PyProperty(PyObject): + """Description of an attribute.""" + + option_spec = PyObject.option_spec.copy() + option_spec.update({ + 'abstractmethod': directives.flag, + 'type': directives.unchanged, + }) + + def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]: + fullname, prefix = super().handle_signature(sig, signode) + + typ = self.options.get('type') + if typ: + signode += addnodes.desc_annotation(typ, ': ' + typ) + + return fullname, prefix + + def get_signature_prefix(self, sig: str) -> str: + prefix = ['property'] + if 'abstractmethod' in self.options: + prefix.insert(0, 'abstract') + + return ' '.join(prefix) + ' ' + + def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str: + name, cls = name_cls + try: + clsname, attrname = name.rsplit('.', 1) + if modname and self.env.config.add_module_names: + clsname = '.'.join([modname, clsname]) + except ValueError: + if modname: + return _('%s (in module %s)') % (name, modname) + else: + return name + + return _('%s (%s property)') % (attrname, clsname) + + class PyDecoratorMixin: """ Mixin for decorator directives. @@ -961,7 +897,7 @@ class PyModule(SphinxDirective): required_arguments = 1 optional_arguments = 0 final_argument_whitespace = False - option_spec = { + option_spec: OptionSpec = { 'platform': lambda x: x, 'synopsis': lambda x: x, 'noindex': directives.flag, @@ -974,7 +910,7 @@ class PyModule(SphinxDirective): modname = self.arguments[0].strip() noindex = 'noindex' in self.options self.env.ref_context['py:module'] = modname - ret = [] # type: List[Node] + ret: List[Node] = [] if not noindex: # note module to the domain node_id = make_id(self.env, self.state.document, 'module', modname) @@ -1025,7 +961,7 @@ class PyCurrentModule(SphinxDirective): required_arguments = 1 optional_arguments = 0 final_argument_whitespace = False - option_spec = {} # type: Dict + option_spec: OptionSpec = {} def run(self) -> List[Node]: modname = self.arguments[0].strip() @@ -1085,10 +1021,9 @@ class PythonModuleIndex(Index): def generate(self, docnames: Iterable[str] = None ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]: - content = {} # type: Dict[str, List[IndexEntry]] + content: Dict[str, List[IndexEntry]] = {} # list of prefixes to ignore - ignores = None # type: List[str] - ignores = self.domain.env.config['modindex_common_prefix'] # type: ignore + ignores: List[str] = self.domain.env.config['modindex_common_prefix'] # type: ignore ignores = sorted(ignores, key=len, reverse=True) # list of all modules, sorted by module name modules = sorted(self.domain.data['modules'].items(), @@ -1151,7 +1086,7 @@ class PythonDomain(Domain): """Python language domain.""" name = 'py' label = 'Python' - object_types = { + object_types: Dict[str, ObjType] = { 'function': ObjType(_('function'), 'func', 'obj'), 'data': ObjType(_('data'), 'data', 'obj'), 'class': ObjType(_('class'), 'class', 'exc', 'obj'), @@ -1160,8 +1095,9 @@ class PythonDomain(Domain): 'classmethod': ObjType(_('class method'), 'meth', 'obj'), 'staticmethod': ObjType(_('static method'), 'meth', 'obj'), 'attribute': ObjType(_('attribute'), 'attr', 'obj'), + 'property': ObjType(_('property'), 'attr', '_prop', 'obj'), 'module': ObjType(_('module'), 'mod', 'obj'), - } # type: Dict[str, ObjType] + } directives = { 'function': PyFunction, @@ -1172,6 +1108,7 @@ class PythonDomain(Domain): 'classmethod': PyClassMethod, 'staticmethod': PyStaticMethod, 'attribute': PyAttribute, + 'property': PyProperty, 'module': PyModule, 'currentmodule': PyCurrentModule, 'decorator': PyDecoratorFunction, @@ -1188,10 +1125,10 @@ class PythonDomain(Domain): 'mod': PyXRefRole(), 'obj': PyXRefRole(), } - initial_data = { + initial_data: Dict[str, Dict[str, Tuple[Any]]] = { 'objects': {}, # fullname -> docname, objtype 'modules': {}, # modname -> docname, synopsis, platform, deprecated - } # type: Dict[str, Dict[str, Tuple[Any]]] + } indices = [ PythonModuleIndex, ] @@ -1200,7 +1137,8 @@ class PythonDomain(Domain): def objects(self) -> Dict[str, ObjectEntry]: return self.data.setdefault('objects', {}) # fullname -> ObjectEntry - def note_object(self, name: str, objtype: str, node_id: str, location: Any = None) -> None: + def note_object(self, name: str, objtype: str, node_id: str, + canonical: bool = False, location: Any = None) -> None: """Note a python object for cross reference. .. versionadded:: 2.1 @@ -1210,7 +1148,7 @@ class PythonDomain(Domain): logger.warning(__('duplicate object description of %s, ' 'other instance in %s, use :noindex: for one of them'), name, other.docname, location=location) - self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype) + self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype, canonical) @property def modules(self) -> Dict[str, ModuleEntry]: @@ -1255,7 +1193,7 @@ class PythonDomain(Domain): if not name: return [] - matches = [] # type: List[Tuple[str, ObjectEntry]] + matches: List[Tuple[str, ObjectEntry]] = [] newname = None if searchmode == 1: @@ -1308,8 +1246,17 @@ class PythonDomain(Domain): type, searchmode) if not matches and type == 'attr': - # fallback to meth (for property) + # fallback to meth (for property; Sphinx-2.4.x) + # this ensures that `:attr:` role continues to refer to the old property entry + # that defined by ``method`` directive in old reST files. matches = self.find_obj(env, modname, clsname, target, 'meth', searchmode) + if not matches and type == 'meth': + # fallback to attr (for property) + # this ensures that `:meth:` in the old reST files can refer to the property + # entry that defined by ``property`` directive. + # + # Note: _prop is a secret role only for internal look-up. + matches = self.find_obj(env, modname, clsname, target, '_prop', searchmode) if not matches: return None @@ -1322,14 +1269,22 @@ class PythonDomain(Domain): if obj[2] == 'module': return self._make_module_refnode(builder, fromdocname, name, contnode) else: - return make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name) + # determine the content of the reference by conditions + content = find_pending_xref_condition(node, 'resolved') + if content: + children = content.children + else: + # if not found, use contnode + children = [contnode] + + return make_refnode(builder, fromdocname, obj[0], obj[1], children, name) def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, contnode: Element ) -> List[Tuple[str, Element]]: modname = node.get('py:module') clsname = node.get('py:class') - results = [] # type: List[Tuple[str, Element]] + results: List[Tuple[str, Element]] = [] # always search in "refspecific" mode with the :any: role matches = self.find_obj(env, modname, clsname, target, None, 1) @@ -1339,9 +1294,17 @@ class PythonDomain(Domain): self._make_module_refnode(builder, fromdocname, name, contnode))) else: + # determine the content of the reference by conditions + content = find_pending_xref_condition(node, 'resolved') + if content: + children = content.children + else: + # if not found, use contnode + children = [contnode] + results.append(('py:' + self.role_for_objtype(obj[2]), make_refnode(builder, fromdocname, obj[0], obj[1], - contnode, name))) + children, name))) return results def _make_module_refnode(self, builder: Builder, fromdocname: str, name: str, @@ -1363,7 +1326,11 @@ class PythonDomain(Domain): yield (modname, modname, 'module', mod.docname, mod.node_id, 0) for refname, obj in self.objects.items(): if obj.objtype != 'module': # modules are already handled - yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1) + if obj.canonical: + # canonical names are not full-text searchable. + yield (refname, refname, obj.objtype, obj.docname, obj.node_id, -1) + else: + yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1) def get_full_qualified_name(self, node: Element) -> str: modname = node.get('py:module') @@ -1384,6 +1351,10 @@ def builtin_resolver(app: Sphinx, env: BuildEnvironment, return s in typing.__all__ # type: ignore + content = find_pending_xref_condition(node, 'resolved') + if content: + contnode = content.children[0] # type: ignore + if node.get('refdomain') != 'py': return None elif node.get('reftype') in ('class', 'obj') and node.get('reftarget') == 'None': @@ -1404,12 +1375,13 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.setup_extension('sphinx.directives') app.add_domain(PythonDomain) + app.add_config_value('python_use_unqualified_type_names', False, 'env') app.connect('object-description-transform', filter_meta_fields) app.connect('missing-reference', builtin_resolver, priority=900) return { 'version': 'builtin', - 'env_version': 2, + 'env_version': 3, 'parallel_read_safe': True, 'parallel_write_safe': True, } |