From b0875d63fc0e57db6b976127015e121059edd698 Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Sat, 25 Feb 2017 20:07:16 -0800 Subject: [PATCH] Fix Python domain nesting Moved #3465 here, to address this in `stable` instead. This fixes a problem with the Python domain object nesting. Because only one object name was stored in `ref_context`, and reset to `None` in `after_content`, nesting broke if you put anything after a nested class: ```rst .. py:class:: Parent .. py:method:: foo() This wouldn't resolve: :py:meth:`bar` .. py:class:: Child In the `after_content` method, the object is reset to `None`, so anything after this in the same nesting is considered to be top level instead. .. py:method:: bar() This is top level, as the domain thinks the surrounding object is `None` ``` This depends on #3519 and can be rebased after that is merged into stable Fixes #3065 Refs #3067 --- sphinx/domains/python.py | 70 ++++++++++++++++++++++++++++++---------- tests/test_domain_py.py | 7 ++-- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index ba38f4702..34240bd89 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -138,6 +138,9 @@ class PyTypedField(PyXrefMixin, TypedField): class PyObject(ObjectDescription): """ Description of a general Python object. + + :cvar allow_nesting: Class is an object that allows for nested namespaces + :vartype allow_nesting: bool """ option_spec = { 'noindex': directives.flag, @@ -164,6 +167,8 @@ class PyObject(ObjectDescription): names=('rtype',), bodyrolename='obj'), ] + allow_nesting = False + def get_signature_prefix(self, sig): """May return a prefix to put before the object name in the signature. @@ -285,12 +290,54 @@ class PyObject(ObjectDescription): fullname, '', None)) def before_content(self): - # needed for automatic qualification of members (reset in subclasses) - self.clsname_set = False + # type: () -> None + """Handle object nesting before content + + If this class is a nestable object, such as a class object, build up a + representation of the nesting heirarchy so that de-nesting multiple + levels works correctly. + + If this class isn't a nestable object, just set the current class name + using the object prefix, if any. This class name will be removed with + :py:meth:`after_content`, and is not added to the list of nested + classes. + """ + prefix = None + if self.names: + (cls_name, cls_name_prefix) = self.names.pop() + prefix = cls_name_prefix.strip('.') if cls_name_prefix else None + if self.allow_nesting: + prefix = cls_name + if prefix: + self.env.ref_context['py:class'] = prefix + if self.allow_nesting: + try: + self.env.ref_context['py:classes'].append(prefix) + except (AttributeError, KeyError): + self.env.ref_context['py:classes'] = [prefix] def after_content(self): - if self.clsname_set: - self.env.ref_context.pop('py:class', None) + # type: () -> None + """Handle object de-nesting after content + + If this class is a nestable object, removing the last nested class prefix + ends further nesting in the object. + + If this class is not a nestable object, the list of classes should not + be altered as we didn't affect the nesting levels in + :py:meth:`before_content`. + """ + if self.allow_nesting: + try: + self.env.ref_context['py:classes'].pop() + except (KeyError, IndexError): + self.env.ref_context['py:classes'] = [] + try: + cls_name = self.env.ref_context.get('py:classes', [])[-1] + except IndexError: + cls_name = None + finally: + self.env.ref_context['py:class'] = cls_name class PyModulelevel(PyObject): @@ -319,6 +366,8 @@ class PyClasslike(PyObject): Description of a class-like object (classes, interfaces, exceptions). """ + allow_nesting = True + def get_signature_prefix(self, sig): return self.objtype + ' ' @@ -332,12 +381,6 @@ class PyClasslike(PyObject): else: return '' - def before_content(self): - PyObject.before_content(self) - if self.names: - self.env.ref_context['py:class'] = self.names[0][0] - self.clsname_set = True - class PyClassmember(PyObject): """ @@ -410,13 +453,6 @@ class PyClassmember(PyObject): else: return '' - def before_content(self): - PyObject.before_content(self) - lastname = self.names and self.names[-1][1] - if lastname and not self.env.ref_context.get('py:class'): - self.env.ref_context['py:class'] = lastname.strip('.') - self.clsname_set = True - class PyDecoratorMixin(object): """ diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index f290bfff3..1f497d69d 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -82,8 +82,8 @@ def test_domain_py_xrefs(app, status, warning): u'subchild_2', u'meth') assert_refnode(refnodes[8], None, u'NestedParentA.NestedChildA', u'NestedParentA.child_1', u'meth') - assert_refnode(refnodes[9], None, None, u'NestedChildA.subchild_1', - u'meth') + assert_refnode(refnodes[9], None, u'NestedParentA', + u'NestedChildA.subchild_1', u'meth') assert_refnode(refnodes[10], None, u'NestedParentB', u'child_1', u'meth') assert_refnode(refnodes[11], None, u'NestedParentB', u'NestedParentB', u'class') @@ -125,6 +125,7 @@ def test_domain_py_objects(app, status, warning): assert objects['module_a.submodule.ModTopLevel'] == ('module', 'class') assert objects['module_a.submodule.ModTopLevel.mod_child_1'] == ('module', 'method') assert objects['module_a.submodule.ModTopLevel.mod_child_2'] == ('module', 'method') + assert 'ModTopLevel.ModNoModule' not in objects assert objects['ModNoModule'] == ('module', 'class') assert objects['module_b.submodule.ModTopLevel'] == ('module', 'class') @@ -136,7 +137,7 @@ def test_domain_py_objects(app, status, warning): assert objects['NestedParentA.NestedChildA'] == ('roles', 'class') assert objects['NestedParentA.NestedChildA.subchild_1'] == ('roles', 'method') assert objects['NestedParentA.NestedChildA.subchild_2'] == ('roles', 'method') - assert objects['child_2'] == ('roles', 'method') + assert objects['NestedParentA.child_2'] == ('roles', 'method') assert objects['NestedParentB'] == ('roles', 'class') assert objects['NestedParentB.child_1'] == ('roles', 'method')