diff --git a/CHANGES b/CHANGES index c8b08a6fd83..c493276529d 100644 --- a/CHANGES +++ b/CHANGES @@ -163,6 +163,7 @@ Bugs fixed rendered as code * #2665, #2607: Link names in C++ docfields, and make it possible for other domains. * #3542: C++, fix parsing error of non-type template argument with template. +* #3065, #3520: python domain fails to recognize nested class Testing -------- @@ -200,6 +201,7 @@ Bugs fixed * #3450:   is appeared in EPUB docs * #3418: Search button is misaligned in nature and pyramid theme * #3421: Could not translate a caption of tables +* #3552: linkcheck raises UnboundLocalError Release 1.5.2 (released Jan 22, 2017) ===================================== diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 30dee277414..274bc994ab5 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,3 +1,5 @@ +.. highlight:: console + Sphinx Developer's Guide ======================== @@ -129,8 +131,8 @@ These are the basic steps needed to start developing on Sphinx. * Run code style checks and type checks (type checks require mypy):: - make style-check - make type-check + make style-check + make type-check * Run the unit tests under different Python environments using :program:`tox`:: @@ -274,14 +276,12 @@ Debugging Tips * Set the debugging options in the `Docutils configuration file `_. -* JavaScript stemming algorithms in `sphinx/search/*.py` (except `en.py`) are +* JavaScript stemming algorithms in ``sphinx/search/*.py`` (except ``en.py``) are generated by this `modified snowballcode generator `_. Generated `JSX `_ files are in `this repository `_. - You can get the resulting JavaScript files using the following command: - - .. code-block:: bash + You can get the resulting JavaScript files using the following command:: $ npm install $ node_modules/.bin/grunt build # -> dest/*.global.js diff --git a/doc/latex.rst b/doc/latex.rst index 56a9e85d9ca..e2c518058c8 100644 --- a/doc/latex.rst +++ b/doc/latex.rst @@ -438,14 +438,6 @@ Let us now list some macros from the package file the new macros are wrappers of the formerly hard-coded ``\texttt``, ``\emph``, ... The default definitions can be found in :file:`sphinx.sty`. -- macros for directional double quotes: pairs of straight double quote ``"`` - in reST source are converted into LaTeX mark-up - ``\sphinxquotedblleft{}`` and ``\sphinxquotedblright{}`` which default to - `````\ ````` and ``''`` (i.e. the TeX mark-up for directional double - quotes via font ligaturing mechanism.) - - .. versionadded:: 1.5.4 - Formerly, produced TeX was directly with `````\ ````` and ``''``. - paragraph level environments: for each admonition type ````, the used environment is named ``sphinx``. They may be ``\renewenvironment`` 'd individually, and must then be defined with one argument (it is the heading diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 99778cddf8f..3693144c370 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -182,7 +182,9 @@ def check_uri(): # history contains any redirects, get last if response.history: code = response.history[-1].status_code - return 'redirected', new_url, code + return 'redirected', new_url, code + else: + return 'redirected', new_url, 0 def check(): # type: () -> Tuple[unicode, unicode, int] diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 1cced3d61ed..d5ff4a60fc3 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -163,6 +163,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, @@ -189,6 +192,8 @@ class PyObject(ObjectDescription): names=('rtype',), bodyrolename='obj'), ] + allow_nesting = False + def get_signature_prefix(self, sig): # type: (unicode) -> unicode """May return a prefix to put before the object name in the @@ -316,13 +321,54 @@ def add_target_and_index(self, name_cls, sig, signode): def before_content(self): # type: () -> None - # needed for automatic qualification of members (reset in subclasses) - self.clsname_set = False + """Handle object nesting before content + + :py:class:`PyObject` represents Python language constructs. For + constructs that are nestable, such as a Python classes, this method will + build up a stack of the nesting heirarchy so that it can be later + de-nested correctly, in :py:meth:`after_content`. + + For constructs that aren't nestable, the stack is bypassed, and instead + only the most recent object is tracked. This object prefix name will be + removed with :py:meth:`after_content`. + """ + if self.names: + # fullname and name_prefix come from the `handle_signature` method. + # fullname represents the full object name that is constructed using + # object nesting and explicit prefixes. `name_prefix` is the + # explicit prefix given in a signature + (fullname, name_prefix) = self.names[-1] + if self.allow_nesting: + prefix = fullname + elif name_prefix: + prefix = name_prefix.strip('.') + else: + prefix = None + if prefix: + self.env.ref_context['py:class'] = prefix + if self.allow_nesting: + classes = self.env.ref_context.setdefault('py:classes', []) + classes.append(prefix) def after_content(self): # type: () -> None - if self.clsname_set: - self.env.ref_context.pop('py:class', 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`. + """ + classes = self.env.ref_context.setdefault('py:classes', []) + if self.allow_nesting: + try: + classes.pop() + except IndexError: + pass + self.env.ref_context['py:class'] = (classes[-1] if len(classes) > 0 + else None) class PyModulelevel(PyObject): @@ -353,6 +399,8 @@ class PyClasslike(PyObject): Description of a class-like object (classes, interfaces, exceptions). """ + allow_nesting = True + def get_signature_prefix(self, sig): # type: (unicode) -> unicode return self.objtype + ' ' @@ -368,13 +416,6 @@ def get_index_text(self, modname, name_cls): else: return '' - def before_content(self): - # type: () -> None - PyObject.before_content(self) - if self.names: - self.env.ref_context['py:class'] = self.names[0][0] - self.clsname_set = True - class PyClassmember(PyObject): """ @@ -450,14 +491,6 @@ def get_index_text(self, modname, name_cls): else: return '' - def before_content(self): - # type: () -> None - 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/sphinx/util/requests.py b/sphinx/util/requests.py index ec5aa232ef9..fcce42f09e1 100644 --- a/sphinx/util/requests.py +++ b/sphinx/util/requests.py @@ -36,30 +36,34 @@ # for requests < 2.4.0 InsecureRequestWarning = None -# try to load requests[security] +# try to load requests[security] (but only if SSL is available) try: - pkg_resources.require(['requests[security]']) -except (pkg_resources.DistributionNotFound, - pkg_resources.VersionConflict): import ssl - if not getattr(ssl, 'HAS_SNI', False): - # don't complain on each url processed about the SSL issue - requests.packages.urllib3.disable_warnings( - requests.packages.urllib3.exceptions.InsecurePlatformWarning) +except ImportError: + pass +else: + try: + pkg_resources.require(['requests[security]']) + except (pkg_resources.DistributionNotFound, + pkg_resources.VersionConflict): + if not getattr(ssl, 'HAS_SNI', False): + # don't complain on each url processed about the SSL issue + requests.packages.urllib3.disable_warnings( + requests.packages.urllib3.exceptions.InsecurePlatformWarning) + warnings.warn( + 'Some links may return broken results due to being unable to ' + 'check the Server Name Indication (SNI) in the returned SSL cert ' + 'against the hostname in the url requested. Recommended to ' + 'install "requests[security]" as a dependency or upgrade to ' + 'a python version with SNI support (Python 3 and Python 2.7.9+).' + ) + except pkg_resources.UnknownExtra: warnings.warn( 'Some links may return broken results due to being unable to ' 'check the Server Name Indication (SNI) in the returned SSL cert ' 'against the hostname in the url requested. Recommended to ' - 'install "requests[security]" as a dependency or upgrade to ' - 'a python version with SNI support (Python 3 and Python 2.7.9+).' + 'install requests-2.4.1+.' ) -except pkg_resources.UnknownExtra: - warnings.warn( - 'Some links may return broken results due to being unable to ' - 'check the Server Name Indication (SNI) in the returned SSL cert ' - 'against the hostname in the url requested. Recommended to ' - 'install requests-2.4.1+.' - ) if False: # For type annotation diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index f290bfff355..1f497d69de4 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -82,8 +82,8 @@ def assert_refnode(node, module_name, class_name, target, reftype=None, 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') diff --git a/tests/test_markup.py b/tests/test_markup.py index 1d5407a279b..11776cf4439 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -97,9 +97,9 @@ def verify(rst, latex_expected): def verify_re(verify_re_html, verify_re_latex): def verify_re_(rst, html_expected, latex_expected): if html_expected: - return verify_re_html(rst, html_expected) + verify_re_html(rst, html_expected) if latex_expected: - return verify_re_latex(rst, latex_expected) + verify_re_latex(rst, latex_expected) return verify_re_ @@ -107,9 +107,9 @@ def verify_re_(rst, html_expected, latex_expected): def verify(verify_re_html, verify_re_latex): def verify_(rst, html_expected, latex_expected): if html_expected: - return verify_re_html(rst, re.escape(html_expected) + '$') + verify_re_html(rst, re.escape(html_expected) + '$') if latex_expected: - return verify_re_latex(rst, re.escape(latex_expected) + '$') + verify_re_latex(rst, re.escape(latex_expected) + '$') return verify_ @@ -179,7 +179,7 @@ def get(name): 'verify', '"John"', '

“John”

', - "``John''", + r'\sphinxquotedblleft{}John\sphinxquotedblright{}', ), ( # ... but not in literal text