diff options
50 files changed, 494 insertions, 371 deletions
@@ -29,7 +29,7 @@ Other contributors, listed alphabetically, are: * Horst Gutmann -- internationalization support * Martin Hans -- autodoc improvements * Doug Hellmann -- graphviz improvements -* Timotheus Kampik - JS enhancements, stop words language fix +* Timotheus Kampik - JS theme & search enhancements * Takeshi Komiya -- numref feature * Dave Kuhlman -- original LaTeX writer * Blaise Laflamme -- pyramid theme @@ -1,3 +1,28 @@ +Release 1.5 (in development) +============================ + +Incompatible changes +-------------------- + +* LaTeX package fancybox is not longer a dependency of sphinx.sty +* Use ``'locales'`` as a default value of `locale_dirs` + +Features added +-------------- + +* Add ``:caption:`` option for sphinx.ext.inheritance_diagram. +* #894: Add ``lualatexpdf`` and ``xelatexpdf`` as a make target to build PDF using lualatex or xelatex +* #2471: Add config variable for default doctest flags. +* Convert linkcheck builder to requests for better encoding handling +* #2463, #2516: Add keywords of "meta" directive to search index + +Bugs fixed +---------- + +Documentation +------------- + + Release 1.4.2 (in development) ============================== diff --git a/doc/_templates/index.html b/doc/_templates/index.html index 30373f06b..2053610f0 100644 --- a/doc/_templates/index.html +++ b/doc/_templates/index.html @@ -13,9 +13,8 @@ Sphinx is a tool that makes it easy to create intelligent and beautiful documentation, written by Georg Brandl and licensed under the BSD license.{%endtrans%}</p> <p>{%trans%}It was originally created for <a href="https://docs.python.org/">the - new Python documentation</a>, and it has excellent facilities for the - documentation of Python projects, but C/C++ is already supported as well, - and it is planned to add special support for other languages as well. Of + Python documentation</a>, and it has excellent facilities for the + documentation of software projects in a range of languages. Of course, this site is also created from reStructuredText sources using Sphinx! The following features should be highlighted:{%endtrans%} </p> diff --git a/doc/config.rst b/doc/config.rst index 47e6da8ef..d4c2784aa 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -516,7 +516,10 @@ documentation on :ref:`intl` for details. :file:`./locale/{language}/LC_MESSAGES/sphinx.mo`. The text domain of individual documents depends on :confval:`gettext_compact`. - The default is ``[]``. + The default is ``['locales']``. + + .. versionchanged:: 1.5 + Use ``locales`` directory as a default value .. confval:: gettext_compact diff --git a/doc/ext/doctest.rst b/doc/ext/doctest.rst index 2d06b69e6..3d4f66a9a 100644 --- a/doc/ext/doctest.rst +++ b/doc/ext/doctest.rst @@ -59,12 +59,9 @@ a comma-separated list of group names. .. rst:directive:: .. doctest:: [group] A doctest-style code block. You can use standard :mod:`doctest` flags for - controlling how actual output is compared with what you give as output. By - default, these options are enabled: ``ELLIPSIS`` (allowing you to put - ellipses in the expected output that match anything in the actual output), - ``IGNORE_EXCEPTION_DETAIL`` (not comparing tracebacks), - ``DONT_ACCEPT_TRUE_FOR_1`` (by default, doctest accepts "True" in the output - where "1" is given -- this is a relic of pre-Python 2.2 times). + controlling how actual output is compared with what you give as output. The + default set of flags is specified by the :confval:`doctest_default_flags` + configuration variable. This directive supports two options: @@ -182,6 +179,20 @@ Configuration The doctest extension uses the following configuration values: +.. confval:: doctest_default_flags + + By default, these options are enabled: + + - ``ELLIPSIS``, allowing you to put ellipses in the expected output that + match anything in the actual output; + - ``IGNORE_EXCEPTION_DETAIL``, causing everything following the leftmost + colon and any module information in the exception name to be ignored; + - ``DONT_ACCEPT_TRUE_FOR_1``, rejecting "True" in the output where "1" is + given -- the default behavior of accepting this substitution is a relic of + pre-Python 2.2 times. + + .. versionadded:: 1.5 + .. confval:: doctest_path A list of directories that will be added to :data:`sys.path` when the doctest diff --git a/doc/ext/inheritance.rst b/doc/ext/inheritance.rst index 5e0a76fcc..dd8d5aa99 100644 --- a/doc/ext/inheritance.rst +++ b/doc/ext/inheritance.rst @@ -33,10 +33,15 @@ It adds this directive: It also supports a ``private-bases`` flag option; if given, private base classes (those whose name starts with ``_``) will be included. + You can use ``caption`` option to give a caption to the diagram. + .. versionchanged:: 1.1 Added ``private-bases`` option; previously, all bases were always included. + .. versionchanged:: 1.5 + Added ``caption`` option + New config values are: diff --git a/doc/ext/napoleon.rst b/doc/ext/napoleon.rst index 304d8ac22..2f2fb4376 100644 --- a/doc/ext/napoleon.rst +++ b/doc/ext/napoleon.rst @@ -371,6 +371,22 @@ enabled in `conf.py`:: * **arg2** (*int, optional*) -- Description of `arg2`, defaults to 0 +.. confval:: napoleon_use_keyword + + True to use a ``:keyword:`` role for each function keyword argument. + False to use a single ``:keyword arguments:`` role for all the + keywords. + *Defaults to True.* + + This behaves similarly to :attr:`napoleon_use_param`. Note unlike docutils, + ``:keyword:`` and ``:param:`` will not be treated the same way - there will + be a separate "Keyword Arguments" section, rendered in the same fashion as + "Parameters" section (type links created if possible) + + .. seealso:: + + :attr:`napoleon_use_param` + .. confval:: napoleon_use_rtype True to use the ``:rtype:`` role for the return type. False to output diff --git a/doc/theming.rst b/doc/theming.rst index f124542c7..df0755f1f 100644 --- a/doc/theming.rst +++ b/doc/theming.rst @@ -273,7 +273,7 @@ These themes are: .. versionchanged:: 1.3 The 'default' theme has been renamed to 'classic'. 'default' is still - available, however it will emit notice a recommendation that using new + available, however it will emit a notice that it is an alias for the new 'alabaster' theme. Creating themes @@ -49,6 +49,7 @@ requires = [ 'babel>=1.3,!=2.0', 'alabaster>=0.7,<0.8', 'imagesize', + 'requests', ] extras_require = { # Environment Marker works for wheel 0.24 or later diff --git a/sphinx/__init__.py b/sphinx/__init__.py index 4e5e97a04..e28316f8d 100644 --- a/sphinx/__init__.py +++ b/sphinx/__init__.py @@ -15,13 +15,13 @@ import sys from os import path -__version__ = '1.4.1+' -__released__ = '1.4.1' # used when Sphinx builds its own docs +__version__ = '1.5a0' +__released__ = '1.5a0' # used when Sphinx builds its own docs # version info for better programmatic use # possible values for 3rd element: 'alpha', 'beta', 'rc', 'final' # 'final' has 0 as the last element -version_info = (1, 4, 2, 'beta', 1) +version_info = (1, 5, 0, 'alpha', 0) package_dir = path.abspath(path.dirname(__file__)) diff --git a/sphinx/apidoc.py b/sphinx/apidoc.py index 58724fd5a..c0639acdf 100644 --- a/sphinx/apidoc.py +++ b/sphinx/apidoc.py @@ -62,11 +62,8 @@ def write_file(name, text, opts): print('File %s already exists, skipping.' % fname) else: print('Creating file %s.' % fname) - f = open(fname, 'w') - try: + with open(fname, 'w') as f: f.write(text) - finally: - f.close() def format_heading(level, text): diff --git a/sphinx/builders/applehelp.py b/sphinx/builders/applehelp.py index 53a0c99ad..d3ad861dc 100644 --- a/sphinx/builders/applehelp.py +++ b/sphinx/builders/applehelp.py @@ -178,14 +178,11 @@ class AppleHelpBuilder(StandaloneHTMLBuilder): # Build the access page self.info(bold('building access page...'), nonl=True) - f = codecs.open(path.join(language_dir, '_access.html'), 'w') - try: + with codecs.open(path.join(language_dir, '_access.html'), 'w') as f: f.write(access_page_template % { 'toc': htmlescape(toc, quote=True), 'title': htmlescape(self.config.applehelp_title) }) - finally: - f.close() self.info('done') # Generate the help index diff --git a/sphinx/builders/changes.py b/sphinx/builders/changes.py index c077b7dd2..ed9edc403 100644 --- a/sphinx/builders/changes.py +++ b/sphinx/builders/changes.py @@ -101,16 +101,10 @@ class ChangesBuilder(Builder): 'show_copyright': self.config.html_show_copyright, 'show_sphinx': self.config.html_show_sphinx, } - f = codecs.open(path.join(self.outdir, 'index.html'), 'w', 'utf8') - try: + with codecs.open(path.join(self.outdir, 'index.html'), 'w', 'utf8') as f: f.write(self.templates.render('changes/frameset.html', ctx)) - finally: - f.close() - f = codecs.open(path.join(self.outdir, 'changes.html'), 'w', 'utf8') - try: + with codecs.open(path.join(self.outdir, 'changes.html'), 'w', 'utf8') as f: f.write(self.templates.render('changes/versionchanges.html', ctx)) - finally: - f.close() hltext = ['.. versionadded:: %s' % version, '.. versionchanged:: %s' % version, @@ -126,27 +120,22 @@ class ChangesBuilder(Builder): self.info(bold('copying source files...')) for docname in self.env.all_docs: - f = codecs.open(self.env.doc2path(docname), 'r', - self.env.config.source_encoding) - try: - lines = f.readlines() - except UnicodeDecodeError: - self.warn('could not read %r for changelog creation' % docname) - continue - finally: - f.close() + with codecs.open(self.env.doc2path(docname), 'r', + self.env.config.source_encoding) as f: + try: + lines = f.readlines() + except UnicodeDecodeError: + self.warn('could not read %r for changelog creation' % docname) + continue targetfn = path.join(self.outdir, 'rst', os_path(docname)) + '.html' ensuredir(path.dirname(targetfn)) - f = codecs.open(targetfn, 'w', 'utf-8') - try: + with codecs.open(targetfn, 'w', 'utf-8') as f: text = ''.join(hl(i+1, line) for (i, line) in enumerate(lines)) ctx = { 'filename': self.env.doc2path(docname, None), 'text': text } f.write(self.templates.render('changes/rstsource.html', ctx)) - finally: - f.close() themectx = dict(('theme_' + key, val) for (key, val) in iteritems(self.theme.get_options({}))) copy_static_entry(path.join(package_dir, 'themes', 'default', diff --git a/sphinx/builders/devhelp.py b/sphinx/builders/devhelp.py index 62e2c9843..eb3e997d8 100644 --- a/sphinx/builders/devhelp.py +++ b/sphinx/builders/devhelp.py @@ -127,8 +127,5 @@ class DevhelpBuilder(StandaloneHTMLBuilder): write_index(title, refs, subitems) # Dump the XML file - f = comp_open(path.join(outdir, outname + '.devhelp'), 'w') - try: + with comp_open(path.join(outdir, outname + '.devhelp'), 'w') as f: tree.write(f, 'utf-8') - finally: - f.close() diff --git a/sphinx/builders/epub.py b/sphinx/builders/epub.py index cc839d757..349574ae0 100644 --- a/sphinx/builders/epub.py +++ b/sphinx/builders/epub.py @@ -496,11 +496,8 @@ class EpubBuilder(StandaloneHTMLBuilder): def build_mimetype(self, outdir, outname): """Write the metainfo file mimetype.""" self.info('writing %s file...' % outname) - f = codecs.open(path.join(outdir, outname), 'w', 'utf-8') - try: + with codecs.open(path.join(outdir, outname), 'w', 'utf-8') as f: f.write(self.mimetype_template) - finally: - f.close() def build_container(self, outdir, outname): """Write the metainfo file META-INF/cointainer.xml.""" @@ -511,11 +508,8 @@ class EpubBuilder(StandaloneHTMLBuilder): except OSError as err: if err.errno != EEXIST: raise - f = codecs.open(path.join(outdir, outname), 'w', 'utf-8') - try: + with codecs.open(path.join(outdir, outname), 'w', 'utf-8') as f: f.write(self.container_template) - finally: - f.close() def content_metadata(self, files, spine, guide): """Create a dictionary with all metadata for the content.opf @@ -652,12 +646,9 @@ class EpubBuilder(StandaloneHTMLBuilder): guide = '\n'.join(guide) # write the project file - f = codecs.open(path.join(outdir, outname), 'w', 'utf-8') - try: + with codecs.open(path.join(outdir, outname), 'w', 'utf-8') as f: f.write(content_tmpl % self.content_metadata(projectfiles, spine, guide)) - finally: - f.close() def new_navpoint(self, node, level, incr=True): """Create a new entry in the toc from the node at given level.""" @@ -749,11 +740,8 @@ class EpubBuilder(StandaloneHTMLBuilder): navpoints = self.build_navpoints(refnodes) level = max(item['level'] for item in self.refnodes) level = min(level, self.config.epub_tocdepth) - f = codecs.open(path.join(outdir, outname), 'w', 'utf-8') - try: + with codecs.open(path.join(outdir, outname), 'w', 'utf-8') as f: f.write(self.toc_template % self.toc_metadata(level, navpoints)) - finally: - f.close() def build_epub(self, outdir, outname): """Write the epub file. diff --git a/sphinx/builders/epub3.py b/sphinx/builders/epub3.py index 0fd347735..b243486f6 100644 --- a/sphinx/builders/epub3.py +++ b/sphinx/builders/epub3.py @@ -203,11 +203,9 @@ class Epub3Builder(EpubBuilder): # 'includehidden' refnodes = self.refnodes navlist = self.build_navlist(refnodes) - f = codecs.open(path.join(outdir, outname), 'w', 'utf-8') - try: + with codecs.open(path.join(outdir, outname), 'w', 'utf-8') as f: f.write(self.navigation_doc_template % self.navigation_doc_metadata(navlist)) - finally: - f.close() - # Add nav.xhtml to epub file - self.files.append(outname) + + # Add nav.xhtml to epub file + self.files.append(outname) diff --git a/sphinx/builders/gettext.py b/sphinx/builders/gettext.py index 1c4789392..c8f4dab4f 100644 --- a/sphinx/builders/gettext.py +++ b/sphinx/builders/gettext.py @@ -211,8 +211,7 @@ class MessageCatalogBuilder(I18nBuilder): ensuredir(path.join(self.outdir, path.dirname(textdomain))) pofn = path.join(self.outdir, textdomain + '.pot') - pofile = open(pofn, 'w', encoding='utf-8') - try: + with open(pofn, 'w', encoding='utf-8') as pofile: pofile.write(POHEADER % data) for message in catalog.messages: @@ -234,6 +233,3 @@ class MessageCatalogBuilder(I18nBuilder): replace('"', r'\"'). \ replace('\n', '\\n"\n"') pofile.write('msgid "%s"\nmsgstr ""\n\n' % message) - - finally: - pofile.close() diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index f541daa1a..63ccdd66c 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -175,8 +175,7 @@ class StandaloneHTMLBuilder(Builder): self.tags_hash = get_stable_hash(sorted(self.tags)) old_config_hash = old_tags_hash = '' try: - fp = open(path.join(self.outdir, '.buildinfo')) - try: + with open(path.join(self.outdir, '.buildinfo')) as fp: version = fp.readline() if version.rstrip() != '# Sphinx build info version 1': raise ValueError @@ -187,8 +186,6 @@ class StandaloneHTMLBuilder(Builder): tag, old_tags_hash = fp.readline().strip().split(': ') if tag != 'tags': raise ValueError - finally: - fp.close() except ValueError: self.warn('unsupported build info format in %r, building all' % path.join(self.outdir, '.buildinfo')) @@ -657,15 +654,12 @@ class StandaloneHTMLBuilder(Builder): def write_buildinfo(self): # write build info file - fp = open(path.join(self.outdir, '.buildinfo'), 'w') - try: + with open(path.join(self.outdir, '.buildinfo'), 'w') as fp: fp.write('# Sphinx build info version 1\n' '# This file hashes the configuration used when building' ' these files. When it is not found, a full rebuild will' ' be done.\nconfig: %s\ntags: %s\n' % (self.config_hash, self.tags_hash)) - finally: - fp.close() def cleanup(self): # clean up theme stuff @@ -705,10 +699,8 @@ class StandaloneHTMLBuilder(Builder): f = codecs.open(searchindexfn, 'r', encoding='utf-8') else: f = open(searchindexfn, 'rb') - try: + with f: self.indexer.load(f, self.indexer_format) - finally: - f.close() except (IOError, OSError, ValueError): if keep: self.warn('search index couldn\'t be loaded, but not all ' @@ -812,11 +804,8 @@ class StandaloneHTMLBuilder(Builder): # outfilename's path is in general different from self.outdir ensuredir(path.dirname(outfilename)) try: - f = codecs.open(outfilename, 'w', encoding, 'xmlcharrefreplace') - try: + with codecs.open(outfilename, 'w', encoding, 'xmlcharrefreplace') as f: f.write(output) - finally: - f.close() except (IOError, OSError) as err: self.warn("error writing file %s: %s" % (outfilename, err)) if self.copysource and ctx.get('sourcename'): @@ -833,8 +822,7 @@ class StandaloneHTMLBuilder(Builder): def dump_inventory(self): self.info(bold('dumping object inventory... '), nonl=True) - f = open(path.join(self.outdir, INVENTORY_FILENAME), 'wb') - try: + with open(path.join(self.outdir, INVENTORY_FILENAME), 'wb') as f: f.write((u'# Sphinx inventory version 2\n' u'# Project: %s\n' u'# Version: %s\n' @@ -856,8 +844,6 @@ class StandaloneHTMLBuilder(Builder): (u'%s %s:%s %s %s %s\n' % (name, domainname, type, prio, uri, dispname)).encode('utf-8'))) f.write(compressor.flush()) - finally: - f.close() self.info('done') def dump_search_index(self): @@ -872,10 +858,8 @@ class StandaloneHTMLBuilder(Builder): f = codecs.open(searchindexfn + '.tmp', 'w', encoding='utf-8') else: f = open(searchindexfn + '.tmp', 'wb') - try: + with f: self.indexer.dump(f, self.indexer_format) - finally: - f.close() movefile(searchindexfn + '.tmp', searchindexfn) self.info('done') @@ -1086,10 +1070,8 @@ class SerializingHTMLBuilder(StandaloneHTMLBuilder): f = codecs.open(filename, 'w', encoding='utf-8') else: f = open(filename, 'wb') - try: + with f: self.implementation.dump(context, f, *self.additional_dump_args) - finally: - f.close() def handle_page(self, pagename, ctx, templatename='page.html', outfilename=None, event_arg=None): diff --git a/sphinx/builders/htmlhelp.py b/sphinx/builders/htmlhelp.py index f4003c4c9..b1a5d7dda 100644 --- a/sphinx/builders/htmlhelp.py +++ b/sphinx/builders/htmlhelp.py @@ -198,16 +198,12 @@ class HTMLHelpBuilder(StandaloneHTMLBuilder): def build_hhx(self, outdir, outname): self.info('dumping stopword list...') - f = self.open_file(outdir, outname+'.stp') - try: + with self.open_file(outdir, outname+'.stp') as f: for word in sorted(stopwords): print(word, file=f) - finally: - f.close() self.info('writing project file...') - f = self.open_file(outdir, outname+'.hhp') - try: + with self.open_file(outdir, outname+'.hhp') as f: f.write(project_template % {'outname': outname, 'title': self.config.html_title, 'version': self.config.version, @@ -223,12 +219,9 @@ class HTMLHelpBuilder(StandaloneHTMLBuilder): fn.endswith('.html'): print(path.join(root, fn)[olen:].replace(os.sep, '\\'), file=f) - finally: - f.close() self.info('writing TOC file...') - f = self.open_file(outdir, outname+'.hhc') - try: + with self.open_file(outdir, outname+'.hhc') as f: f.write(contents_header) # special books f.write('<LI> ' + object_sitemap % (self.config.html_short_title, @@ -266,13 +259,10 @@ class HTMLHelpBuilder(StandaloneHTMLBuilder): for node in tocdoc.traverse(istoctree): write_toc(node) f.write(contents_footer) - finally: - f.close() self.info('writing index file...') index = self.env.create_index(self) - f = self.open_file(outdir, outname+'.hhk') - try: + with self.open_file(outdir, outname+'.hhk') as f: f.write('<UL>\n') def write_index(title, refs, subitems): @@ -302,5 +292,3 @@ class HTMLHelpBuilder(StandaloneHTMLBuilder): for title, (refs, subitems, key_) in group: write_index(title, refs, subitems) f.write('</UL>\n') - finally: - f.close() diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 0533f1c50..700ea2880 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -14,11 +14,13 @@ import socket import codecs import threading from os import path +import warnings +import pkg_resources +import requests +from requests.exceptions import HTTPError from six.moves import queue -from six.moves.urllib.request import build_opener, Request, HTTPRedirectHandler from six.moves.urllib.parse import unquote -from six.moves.urllib.error import HTTPError from six.moves.html_parser import HTMLParser from docutils import nodes @@ -36,28 +38,25 @@ from sphinx.builders import Builder from sphinx.util import encode_uri from sphinx.util.console import purple, red, darkgreen, darkgray, \ darkred, turquoise -from sphinx.util.pycompat import TextIOWrapper - -class RedirectHandler(HTTPRedirectHandler): - """A RedirectHandler that records the redirect code we got.""" - - def redirect_request(self, req, fp, code, msg, headers, newurl): - new_req = HTTPRedirectHandler.redirect_request(self, req, fp, code, - msg, headers, newurl) - req.redirect_code = code - return new_req - -# create an opener that will simulate a browser user-agent -opener = build_opener(RedirectHandler) -opener.addheaders = [('User-agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:25.0) ' - 'Gecko/20100101 Firefox/25.0')] - - -class HeadRequest(Request): - """Subclass of urllib2.Request that sends a HEAD request.""" - def get_method(self): - return 'HEAD' +try: + pkg_resources.require(['requests[security]']) +except pkg_resources.DistributionNotFound: + 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) + 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+).' + ) + +requests_user_agent = [('User-agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:25.0) ' + 'Gecko/20100101 Firefox/25.0')] class AnchorCheckParser(HTMLParser): @@ -75,18 +74,18 @@ class AnchorCheckParser(HTMLParser): self.found = True -def check_anchor(f, anchor): - """Reads HTML data from a filelike object 'f' searching for *anchor*. +def check_anchor(response, anchor): + """Reads HTML data from a response object `response` searching for `anchor`. Returns True if anchor was found, False otherwise. """ parser = AnchorCheckParser(anchor) try: - # Read file in chunks of 8192 bytes. If we find a matching anchor, we - # break the loop early in hopes not to have to download the whole thing. - chunk = f.read(8192) - while chunk and not parser.found: + # Read file in chunks. If we find a matching anchor, we break + # the loop early in hopes not to have to download the whole thing. + for chunk in response.iter_content(): parser.feed(chunk) - chunk = f.read(8192) + if parser.found: + break parser.close() except HTMLParseError: # HTMLParser is usually pretty good with sloppy HTML, but it tends to @@ -95,17 +94,6 @@ def check_anchor(f, anchor): return parser.found -def get_content_charset(f): - content_type = f.headers.get('content-type') - if content_type: - params = (p.strip() for p in content_type.split(';')[1:]) - for param in params: - if param.startswith('charset='): - return param[8:] - - return None - - class CheckExternalLinksBuilder(Builder): """ Checks for broken external links. @@ -122,6 +110,9 @@ class CheckExternalLinksBuilder(Builder): # create output file open(path.join(self.outdir, 'output.txt'), 'w').close() + self.session = requests.Session() + self.session.headers = dict(requests_user_agent) + # create queues and worker threads self.wqueue = queue.Queue() self.rqueue = queue.Queue() @@ -137,6 +128,8 @@ class CheckExternalLinksBuilder(Builder): if self.app.config.linkcheck_timeout: kwargs['timeout'] = self.app.config.linkcheck_timeout + kwargs['allow_redirects'] = True + def check_uri(): # split off anchor if '#' in uri: @@ -157,16 +150,8 @@ class CheckExternalLinksBuilder(Builder): # Read the whole document and see if #anchor exists # (Anchors starting with ! are ignored since they are # commonly used for dynamic pages) - req = Request(req_url) - f = opener.open(req, **kwargs) - encoding = 'utf-8' - if hasattr(f.headers, 'get_content_charset'): - encoding = f.headers.get_content_charset() or encoding - else: - encoding = get_content_charset(f) or encoding - found = check_anchor(TextIOWrapper(f, encoding), - unquote(anchor)) - f.close() + response = requests.get(req_url, stream=True, **kwargs) + found = check_anchor(response, unquote(anchor)) if not found: raise Exception("Anchor '%s' not found" % anchor) @@ -174,32 +159,32 @@ class CheckExternalLinksBuilder(Builder): try: # try a HEAD request, which should be easier on # the server and the network - req = HeadRequest(req_url) - f = opener.open(req, **kwargs) - f.close() + response = requests.head(req_url, **kwargs) + response.raise_for_status() except HTTPError as err: - if err.code != 405: + if err.response.status_code not in (403, 405): raise # retry with GET if that fails, some servers - # don't like HEAD requests and reply with 405 - req = Request(req_url) - f = opener.open(req, **kwargs) - f.close() + # don't like HEAD requests and reply with 403 or 405 + response = requests.get(req_url, stream=True, **kwargs) + response.raise_for_status() except HTTPError as err: - if err.code == 401: + if err.response.status_code == 401: # We'll take "Unauthorized" as working. return 'working', ' - unauthorized', 0 else: return 'broken', str(err), 0 except Exception as err: return 'broken', str(err), 0 - if f.url.rstrip('/') == req_url.rstrip('/'): + if response.url.rstrip('/') == req_url.rstrip('/'): return 'working', '', 0 else: - new_url = f.url + new_url = response.url if anchor: new_url += '#' + anchor - code = getattr(req, 'redirect_code', 0) + # history contains any redirects, get last + if response.history: + code = response.history[-1].status_code return 'redirected', new_url, code def check(): diff --git a/sphinx/builders/qthelp.py b/sphinx/builders/qthelp.py index 0a7e85c92..4139c92c6 100644 --- a/sphinx/builders/qthelp.py +++ b/sphinx/builders/qthelp.py @@ -179,8 +179,7 @@ class QtHelpBuilder(StandaloneHTMLBuilder): nspace = nspace.lower() # write the project file - f = codecs.open(path.join(outdir, outname+'.qhp'), 'w', 'utf-8') - try: + with codecs.open(path.join(outdir, outname+'.qhp'), 'w', 'utf-8') as f: f.write(project_template % { 'outname': htmlescape(outname), 'title': htmlescape(self.config.html_title), @@ -191,23 +190,18 @@ class QtHelpBuilder(StandaloneHTMLBuilder): 'sections': sections, 'keywords': keywords, 'files': projectfiles}) - finally: - f.close() homepage = 'qthelp://' + posixpath.join( nspace, 'doc', self.get_target_uri(self.config.master_doc)) startpage = 'qthelp://' + posixpath.join(nspace, 'doc', 'index.html') self.info('writing collection project file...') - f = codecs.open(path.join(outdir, outname+'.qhcp'), 'w', 'utf-8') - try: + with codecs.open(path.join(outdir, outname+'.qhcp'), 'w', 'utf-8') as f: f.write(collection_template % { 'outname': htmlescape(outname), 'title': htmlescape(self.config.html_short_title), 'homepage': htmlescape(homepage), 'startpage': htmlescape(startpage)}) - finally: - f.close() def isdocnode(self, node): if not isinstance(node, nodes.list_item): diff --git a/sphinx/builders/texinfo.py b/sphinx/builders/texinfo.py index dec278c86..8c4bd2419 100644 --- a/sphinx/builders/texinfo.py +++ b/sphinx/builders/texinfo.py @@ -220,11 +220,8 @@ class TexinfoBuilder(Builder): fn = path.join(self.outdir, 'Makefile') self.info(fn, nonl=1) try: - mkfile = open(fn, 'w') - try: + with open(fn, 'w') as mkfile: mkfile.write(TEXINFO_MAKEFILE) - finally: - mkfile.close() except (IOError, OSError) as err: self.warn("error writing file %s: %s" % (fn, err)) self.info(' done') diff --git a/sphinx/builders/text.py b/sphinx/builders/text.py index 85da4a1a2..202ec20db 100644 --- a/sphinx/builders/text.py +++ b/sphinx/builders/text.py @@ -60,11 +60,8 @@ class TextBuilder(Builder): outfilename = path.join(self.outdir, os_path(docname) + self.out_suffix) ensuredir(path.dirname(outfilename)) try: - f = codecs.open(outfilename, 'w', 'utf-8') - try: + with codecs.open(outfilename, 'w', 'utf-8') as f: f.write(self.writer.output) - finally: - f.close() except (IOError, OSError) as err: self.warn("error writing file %s: %s" % (outfilename, err)) diff --git a/sphinx/builders/xml.py b/sphinx/builders/xml.py index 91cb273f5..589e8a63a 100644 --- a/sphinx/builders/xml.py +++ b/sphinx/builders/xml.py @@ -77,11 +77,8 @@ class XMLBuilder(Builder): outfilename = path.join(self.outdir, os_path(docname) + self.out_suffix) ensuredir(path.dirname(outfilename)) try: - f = codecs.open(outfilename, 'w', 'utf-8') - try: + with codecs.open(outfilename, 'w', 'utf-8') as f: f.write(self.writer.output) - finally: - f.close() except (IOError, OSError) as err: self.warn("error writing file %s: %s" % (outfilename, err)) diff --git a/sphinx/config.py b/sphinx/config.py index 9cbf655f8..ef57334fe 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -59,7 +59,7 @@ class Config(object): today_fmt = (None, 'env', string_classes), language = (None, 'env', string_classes), - locale_dirs = ([], 'env'), + locale_dirs = (['locales'], 'env'), figure_language_filename = (u'{root}.{language}{ext}', 'env', [str]), master_doc = ('contents', 'env'), @@ -86,8 +86,8 @@ class Config(object): primary_domain = ('py', 'env', [NoneType]), needs_sphinx = (None, None, string_classes), needs_extensions = ({}, None), - nitpicky = (False, 'env'), - nitpick_ignore = ([], 'html'), + nitpicky = (False, None), + nitpick_ignore = ([], None), numfig = (False, 'env'), numfig_secnum_depth = (1, 'env'), numfig_format = ({'figure': l_('Fig. %s'), diff --git a/sphinx/directives/code.py b/sphinx/directives/code.py index fac8f6419..de804f988 100644 --- a/sphinx/directives/code.py +++ b/sphinx/directives/code.py @@ -173,13 +173,12 @@ class LiteralInclude(Directive): } def read_with_encoding(self, filename, document, codec_info, encoding): - f = None try: - f = codecs.StreamReaderWriter(open(filename, 'rb'), codec_info[2], - codec_info[3], 'strict') - lines = f.readlines() - lines = dedent_lines(lines, self.options.get('dedent')) - return lines + with codecs.StreamReaderWriter(open(filename, 'rb'), codec_info[2], + codec_info[3], 'strict') as f: + lines = f.readlines() + lines = dedent_lines(lines, self.options.get('dedent')) + return lines except (IOError, OSError): return [document.reporter.warning( 'Include file %r not found or reading it failed' % filename, @@ -189,9 +188,6 @@ class LiteralInclude(Directive): 'Encoding %r used for reading included file %r seems to ' 'be wrong, try giving an :encoding: option' % (encoding, filename))] - finally: - if f is not None: - f.close() def run(self): document = self.state.document diff --git a/sphinx/environment.py b/sphinx/environment.py index d5848a429..92064f911 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -103,11 +103,8 @@ class BuildEnvironment: @staticmethod def frompickle(srcdir, config, filename): - picklefile = open(filename, 'rb') - try: + with open(filename, 'rb') as picklefile: env = pickle.load(picklefile) - finally: - picklefile.close() if env.version != ENV_VERSION: raise IOError('build environment version not current') if env.srcdir != srcdir: @@ -123,7 +120,6 @@ class BuildEnvironment: del self.config.values domains = self.domains del self.domains - picklefile = open(filename, 'wb') # remove potentially pickling-problematic values from config for key, val in list(vars(self.config).items()): if key.startswith('_') or \ @@ -131,10 +127,8 @@ class BuildEnvironment: isinstance(val, types.FunctionType) or \ isinstance(val, class_types): del self.config[key] - try: + with open(filename, 'wb') as picklefile: pickle.dump(self, picklefile, pickle.HIGHEST_PROTOCOL) - finally: - picklefile.close() # reset attributes self.domains = domains self.config.values = values @@ -751,12 +745,9 @@ class BuildEnvironment: if self.versioning_compare: # get old doctree try: - f = open(self.doc2path(docname, - self.doctreedir, '.doctree'), 'rb') - try: + with open(self.doc2path(docname, + self.doctreedir, '.doctree'), 'rb') as f: old_doctree = pickle.load(f) - finally: - f.close() except EnvironmentError: pass @@ -786,11 +777,8 @@ class BuildEnvironment: doctree_filename = self.doc2path(docname, self.doctreedir, '.doctree') ensuredir(path.dirname(doctree_filename)) - f = open(doctree_filename, 'wb') - try: + with open(doctree_filename, 'wb') as f: pickle.dump(doctree, f, pickle.HIGHEST_PROTOCOL) - finally: - f.close() # utilities to use while reading a document @@ -1226,11 +1214,8 @@ class BuildEnvironment: def get_doctree(self, docname): """Read the doctree for a file from the pickle and return it.""" doctree_filename = self.doc2path(docname, self.doctreedir, '.doctree') - f = open(doctree_filename, 'rb') - try: + with open(doctree_filename, 'rb') as f: doctree = pickle.load(f) - finally: - f.close() doctree.settings.env = self doctree.reporter = Reporter(self.doc2path(docname), 2, 5, stream=WarningStream(self._warnfunc)) diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index 78281bb85..c08b1e706 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -87,8 +87,7 @@ class CoverageBuilder(Builder): c_objects = self.env.domaindata['c']['objects'] for filename in self.c_sourcefiles: undoc = set() - f = open(filename, 'r') - try: + with open(filename, 'r') as f: for line in f: for key, regex in self.c_regexes: match = regex.match(line) @@ -101,15 +100,12 @@ class CoverageBuilder(Builder): else: undoc.add((key, name)) continue - finally: - f.close() if undoc: self.c_undoc[filename] = undoc def write_c_coverage(self): output_file = path.join(self.outdir, 'c.txt') - op = open(output_file, 'w') - try: + with open(output_file, 'w') as op: if self.config.coverage_write_headline: write_header(op, 'Undocumented C API elements', '=') op.write('\n') @@ -119,8 +115,6 @@ class CoverageBuilder(Builder): for typ, name in sorted(undoc): op.write(' * %-50s [%9s]\n' % (name, typ)) op.write('\n') - finally: - op.close() def build_py_coverage(self): objects = self.env.domaindata['py']['objects'] @@ -214,9 +208,8 @@ class CoverageBuilder(Builder): def write_py_coverage(self): output_file = path.join(self.outdir, 'python.txt') - op = open(output_file, 'w') failed = [] - try: + with open(output_file, 'w') as op: if self.config.coverage_write_headline: write_header(op, 'Undocumented Python objects', '=') keys = sorted(self.py_undoc.keys()) @@ -247,17 +240,12 @@ class CoverageBuilder(Builder): if failed: write_header(op, 'Modules that failed to import') op.writelines(' * %s -- %s\n' % x for x in failed) - finally: - op.close() def finish(self): # dump the coverage data to a pickle file too picklepath = path.join(self.outdir, 'undoc.pickle') - dumpfile = open(picklepath, 'wb') - try: + with open(picklepath, 'wb') as dumpfile: pickle.dump((self.py_undoc, self.c_undoc), dumpfile) - finally: - dumpfile.close() def setup(app): diff --git a/sphinx/ext/doctest.py b/sphinx/ext/doctest.py index 0f5241a19..244762b69 100644 --- a/sphinx/ext/doctest.py +++ b/sphinx/ext/doctest.py @@ -214,8 +214,7 @@ class DocTestBuilder(Builder): def init(self): # default options - self.opt = doctest.DONT_ACCEPT_TRUE_FOR_1 | doctest.ELLIPSIS | \ - doctest.IGNORE_EXCEPTION_DETAIL + self.opt = self.config.doctest_default_flags # HACK HACK HACK # doctest compiles its snippets with type 'single'. That is nice @@ -464,4 +463,8 @@ def setup(app): app.add_config_value('doctest_test_doctest_blocks', 'default', False) app.add_config_value('doctest_global_setup', '', False) app.add_config_value('doctest_global_cleanup', '', False) + app.add_config_value( + 'doctest_default_flags', + doctest.DONT_ACCEPT_TRUE_FOR_1 | doctest.ELLIPSIS | doctest.IGNORE_EXCEPTION_DETAIL, + False) return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index 4e06677ca..527cb3704 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -82,11 +82,8 @@ class Graphviz(Directive): rel_filename, filename = env.relfn2path(argument) env.note_dependency(rel_filename) try: - fp = codecs.open(filename, 'r', 'utf-8') - try: + with codecs.open(filename, 'r', 'utf-8') as fp: dotcode = fp.read() - finally: - fp.close() except (IOError, OSError): return [document.reporter.warning( 'External Graphviz file %r not found or reading ' @@ -239,11 +236,8 @@ def render_dot_html(self, node, code, options, prefix='graphviz', <p class="warning">%s</p></object>\n''' % (fname, alt) self.body.append(svgtag) else: - mapfile = open(outfn + '.map', 'rb') - try: + with open(outfn + '.map', 'rb') as mapfile: imgmap = mapfile.readlines() - finally: - mapfile.close() if len(imgmap) == 2: # nothing in image map (the lines are <map> and </map>) self.body.append('<img src="%s" alt="%s" %s/>\n' % diff --git a/sphinx/ext/inheritance_diagram.py b/sphinx/ext/inheritance_diagram.py index a06c4b17c..11af67dc5 100644 --- a/sphinx/ext/inheritance_diagram.py +++ b/sphinx/ext/inheritance_diagram.py @@ -52,7 +52,7 @@ from docutils.parsers.rst import directives import sphinx from sphinx.ext.graphviz import render_dot_html, render_dot_latex, \ - render_dot_texinfo + render_dot_texinfo, figure_wrapper from sphinx.pycode import ModuleAnalyzer from sphinx.util import force_decode from sphinx.util.compat import Directive @@ -297,6 +297,7 @@ class InheritanceDiagram(Directive): option_spec = { 'parts': directives.nonnegative_int, 'private-bases': directives.flag, + 'caption': directives.unchanged, } def run(self): @@ -330,6 +331,11 @@ class InheritanceDiagram(Directive): # Store the graph object so we can use it to generate the # dot file later node['graph'] = graph + + # wrap the result in figure node + caption = self.options.get('caption') + if caption: + node = figure_wrapper(self, node, caption) return [node] diff --git a/sphinx/ext/napoleon/__init__.py b/sphinx/ext/napoleon/__init__.py index 85c8acec8..9dedc15d7 100644 --- a/sphinx/ext/napoleon/__init__.py +++ b/sphinx/ext/napoleon/__init__.py @@ -33,6 +33,7 @@ class Config(object): # Napoleon settings napoleon_google_docstring = True napoleon_numpy_docstring = True + napoleon_include_init_with_doc = False napoleon_include_private_with_doc = False napoleon_include_special_with_doc = False napoleon_use_admonition_for_examples = False @@ -41,6 +42,7 @@ class Config(object): napoleon_use_ivar = False napoleon_use_param = True napoleon_use_rtype = True + napoleon_use_keyword = True .. _Google style: http://google.github.io/styleguide/pyguide.html @@ -55,6 +57,21 @@ class Config(object): napoleon_numpy_docstring : bool, defaults to True True to parse `NumPy style`_ docstrings. False to disable support for NumPy style docstrings. + napoleon_include_init_with_doc : bool, defaults to False + True to include init methods (i.e. ``__init___``) with + docstrings in the documentation. False to fall back to Sphinx's + default behavior. + + **If True**:: + + def __init__(self): + \"\"\" + This will be included in the docs because it has a docstring + \"\"\" + + def __init__(self): + # This will NOT be included in the docs + napoleon_include_private_with_doc : bool, defaults to False True to include private members (like ``_membername``) with docstrings in the documentation. False to fall back to Sphinx's default behavior. @@ -184,6 +201,20 @@ class Config(object): * **arg2** (*int, optional*) -- Description of `arg2`, defaults to 0 + napoleon_use_keyword : bool, defaults to True + True to use a ``:keyword:`` role for each function keyword argument. + False to use a single ``:keyword arguments:`` role for all the + keywords. + + This behaves similarly to :attr:`napoleon_use_param`. Note unlike docutils, + ``:keyword:`` and ``:param:`` will not be treated the same way - there will + be a separate "Keyword Arguments" section, rendered in the same fashion as + "Parameters" section (type links created if possible) + + See Also + -------- + :attr:`napoleon_use_param` + napoleon_use_rtype : bool, defaults to True True to use the ``:rtype:`` role for the return type. False to output the return type inline with the description. @@ -208,6 +239,7 @@ class Config(object): _config_values = { 'napoleon_google_docstring': (True, 'env'), 'napoleon_numpy_docstring': (True, 'env'), + 'napoleon_include_init_with_doc': (False, 'env'), 'napoleon_include_private_with_doc': (False, 'env'), 'napoleon_include_special_with_doc': (False, 'env'), 'napoleon_use_admonition_for_examples': (False, 'env'), @@ -216,6 +248,7 @@ class Config(object): 'napoleon_use_ivar': (False, 'env'), 'napoleon_use_param': (True, 'env'), 'napoleon_use_rtype': (True, 'env'), + 'napoleon_use_keyword': (True, 'env') } def __init__(self, **settings): @@ -251,6 +284,8 @@ def setup(app): if not isinstance(app, Sphinx): return # probably called by tests + _patch_python_domain() + app.connect('autodoc-process-docstring', _process_docstring) app.connect('autodoc-skip-member', _skip_member) @@ -259,6 +294,23 @@ def setup(app): return {'version': sphinx.__display_version__, 'parallel_read_safe': True} +def _patch_python_domain(): + import sphinx.domains.python + from sphinx.domains.python import PyTypedField + import sphinx.locale + l_ = sphinx.locale.lazy_gettext + for doc_field in sphinx.domains.python.PyObject.doc_field_types: + if doc_field.name == 'parameter': + doc_field.names = ('param', 'parameter', 'arg', 'argument') + break + sphinx.domains.python.PyObject.doc_field_types.append( + PyTypedField('keyword', label=l_('Keyword Arguments'), + names=('keyword', 'kwarg', 'kwparam'), + typerolename='obj', typenames=('paramtype', 'kwtype'), + can_collapse=True), + ) + + def _process_docstring(app, what, name, obj, options, lines): """Process the docstring for a given python object. @@ -311,8 +363,10 @@ def _skip_member(app, what, name, obj, skip, options): """Determine if private and special class members are included in docs. The following settings in conf.py determine if private and special class - members are included in the generated documentation: + members or init methods are included in the generated documentation: + * ``napoleon_include_init_with_doc`` -- + include init methods if they have docstrings * ``napoleon_include_private_with_doc`` -- include private members if they have docstrings * ``napoleon_include_special_with_doc`` -- @@ -349,7 +403,7 @@ def _skip_member(app, what, name, obj, skip, options): """ has_doc = getattr(obj, '__doc__', False) is_member = (what == 'class' or what == 'exception' or what == 'module') - if name != '__weakref__' and name != '__init__' and has_doc and is_member: + if name != '__weakref__' and has_doc and is_member: cls_is_owner = False if what == 'class' or what == 'exception': if PY2: @@ -382,10 +436,16 @@ def _skip_member(app, what, name, obj, skip, options): cls_is_owner = True if what == 'module' or cls_is_owner: - is_special = name.startswith('__') and name.endswith('__') - is_private = not is_special and name.startswith('_') + is_init = (name == '__init__') + is_special = (not is_init and name.startswith('__') and + name.endswith('__')) + is_private = (not is_init and not is_special and + name.startswith('_')) + inc_init = app.config.napoleon_include_init_with_doc inc_special = app.config.napoleon_include_special_with_doc inc_private = app.config.napoleon_include_private_with_doc - if (is_special and inc_special) or (is_private and inc_private): + if ((is_special and inc_special) or + (is_private and inc_private) or + (is_init and inc_init)): return False return skip diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index 742a9fbba..c12b75e6d 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -527,7 +527,15 @@ class GoogleDocstring(UnicodeMixin): return [header, ''] def _parse_keyword_arguments_section(self, section): - return self._format_fields('Keyword Arguments', self._consume_fields()) + fields = self._consume_fields() + if self._config.napoleon_use_keyword: + return self._generate_docutils_params( + fields, + field_role="keyword", + type_role="kwtype" + ) + else: + return self._format_fields('Keyword Arguments', fields) def _parse_methods_section(self, section): lines = [] @@ -552,24 +560,26 @@ class GoogleDocstring(UnicodeMixin): def _parse_parameters_section(self, section): fields = self._consume_fields() if self._config.napoleon_use_param: - lines = [] - for _name, _type, _desc in fields: - _desc = self._strip_empty(_desc) - if any(_desc): - if self._is_list(_desc): - _desc = [''] + _desc - field = ':param %s: ' % _name - lines.extend(self._format_block(field, _desc)) - else: - lines.append(':param %s:' % _name) - - if _type: - lines.append(':type %s: %s' % (_name, _type)) - - return lines + [''] + return self._generate_docutils_params(fields) else: return self._format_fields('Parameters', fields) + def _generate_docutils_params(self, fields, field_role='param', type_role='type'): + lines = [] + for _name, _type, _desc in fields: + _desc = self._strip_empty(_desc) + if any(_desc): + if self._is_list(_desc): + _desc = [''] + _desc + field = ':%s %s: ' % (field_role, _name) + lines.extend(self._format_block(field, _desc)) + else: + lines.append(':%s %s:' % (field_role, _name)) + + if _type: + lines.append(':%s %s: %s' % (type_role, _name, _type)) + return lines + [''] + def _parse_raises_section(self, section): fields = self._consume_fields(parse_type=False, prefer_type=True) field_type = ':raises:' diff --git a/sphinx/jinja2glue.py b/sphinx/jinja2glue.py index f3ce87a8c..c5e0e29e5 100644 --- a/sphinx/jinja2glue.py +++ b/sphinx/jinja2glue.py @@ -70,10 +70,8 @@ class SphinxFileSystemLoader(FileSystemLoader): f = open_if_exists(filename) if f is None: continue - try: + with f: contents = f.read().decode(self.encoding) - finally: - f.close() mtime = path.getmtime(filename) diff --git a/sphinx/make_mode.py b/sphinx/make_mode.py index 67ba9e1e1..9a17895bd 100644 --- a/sphinx/make_mode.py +++ b/sphinx/make_mode.py @@ -30,30 +30,32 @@ proj_name = os.getenv('SPHINXPROJ', '<project>') BUILDERS = [ - ("", "html", "to make standalone HTML files"), - ("", "dirhtml", "to make HTML files named index.html in directories"), - ("", "singlehtml", "to make a single large HTML file"), - ("", "pickle", "to make pickle files"), - ("", "json", "to make JSON files"), - ("", "htmlhelp", "to make HTML files and a HTML help project"), - ("", "qthelp", "to make HTML files and a qthelp project"), - ("", "devhelp", "to make HTML files and a Devhelp project"), - ("", "epub", "to make an epub"), - ("", "latex", "to make LaTeX files, you can set PAPER=a4 or PAPER=letter"), - ("posix", "latexpdf", "to make LaTeX files and run them through pdflatex"), - ("posix", "latexpdfja", "to make LaTeX files and run them through platex/dvipdfmx"), - ("", "text", "to make text files"), - ("", "man", "to make manual pages"), - ("", "texinfo", "to make Texinfo files"), - ("posix", "info", "to make Texinfo files and run them through makeinfo"), - ("", "gettext", "to make PO message catalogs"), - ("", "changes", "to make an overview of all changed/added/deprecated items"), - ("", "xml", "to make Docutils-native XML files"), - ("", "pseudoxml", "to make pseudoxml-XML files for display purposes"), - ("", "linkcheck", "to check all external links for integrity"), - ("", "doctest", "to run all doctests embedded in the documentation " - "(if enabled)"), - ("", "coverage", "to run coverage check of the documentation (if enabled)"), + ("", "html", "to make standalone HTML files"), + ("", "dirhtml", "to make HTML files named index.html in directories"), + ("", "singlehtml", "to make a single large HTML file"), + ("", "pickle", "to make pickle files"), + ("", "json", "to make JSON files"), + ("", "htmlhelp", "to make HTML files and a HTML help project"), + ("", "qthelp", "to make HTML files and a qthelp project"), + ("", "devhelp", "to make HTML files and a Devhelp project"), + ("", "epub", "to make an epub"), + ("", "latex", "to make LaTeX files, you can set PAPER=a4 or PAPER=letter"), + ("posix", "latexpdf", "to make LaTeX files and run them through pdflatex"), + ("posix", "latexpdfja", "to make LaTeX files and run them through platex/dvipdfmx"), + ("posix", "lualatexpdf", "to make LaTeX files and run them through lualatex"), + ("posix", "xelatexpdf", "to make LaTeX files and run them through xelatex"), + ("", "text", "to make text files"), + ("", "man", "to make manual pages"), + ("", "texinfo", "to make Texinfo files"), + ("posix", "info", "to make Texinfo files and run them through makeinfo"), + ("", "gettext", "to make PO message catalogs"), + ("", "changes", "to make an overview of all changed/added/deprecated items"), + ("", "xml", "to make Docutils-native XML files"), + ("", "pseudoxml", "to make pseudoxml-XML files for display purposes"), + ("", "linkcheck", "to check all external links for integrity"), + ("", "doctest", "to run all doctests embedded in the documentation " + "(if enabled)"), + ("", "coverage", "to run coverage check of the documentation (if enabled)"), ] @@ -171,6 +173,18 @@ class Make(object): with cd(self.builddir_join('latex')): os.system('make all-pdf-ja') + def build_lualatexpdf(self): + if self.run_generic_build('latex') > 0: + return 1 + with cd(self.builddir_join('latex')): + os.system('make PDFLATEX=lualatex all-pdf') + + def build_xelatexpdf(self): + if self.run_generic_build('latex') > 0: + return 1 + with cd(self.builddir_join('latex')): + os.system('make PDFLATEX=xelatex all-pdf') + def build_text(self): if self.run_generic_build('text') > 0: return 1 diff --git a/sphinx/pycode/pgen2/driver.py b/sphinx/pycode/pgen2/driver.py index c531edb34..6bdcebece 100644 --- a/sphinx/pycode/pgen2/driver.py +++ b/sphinx/pycode/pgen2/driver.py @@ -92,11 +92,8 @@ class Driver(object): def parse_file(self, filename, debug=False): """Parse a file and return the syntax tree.""" - stream = open(filename) - try: + with open(filename) as stream: return self.parse_stream(stream, debug) - finally: - stream.close() def parse_string(self, text, debug=False): """Parse a string and return the syntax tree.""" diff --git a/sphinx/quickstart.py b/sphinx/quickstart.py index 1f14c0d09..eb54ca9cf 100644 --- a/sphinx/quickstart.py +++ b/sphinx/quickstart.py @@ -545,33 +545,35 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) %(rsrcdir)s .PHONY: help help: \t@echo "Please use \\`make <target>' where <target> is one of" -\t@echo " html to make standalone HTML files" -\t@echo " dirhtml to make HTML files named index.html in directories" -\t@echo " singlehtml to make a single large HTML file" -\t@echo " pickle to make pickle files" -\t@echo " json to make JSON files" -\t@echo " htmlhelp to make HTML files and a HTML help project" -\t@echo " qthelp to make HTML files and a qthelp project" -\t@echo " applehelp to make an Apple Help Book" -\t@echo " devhelp to make HTML files and a Devhelp project" -\t@echo " epub to make an epub" -\t@echo " epub3 to make an epub3" -\t@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" -\t@echo " latexpdf to make LaTeX files and run them through pdflatex" -\t@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" -\t@echo " text to make text files" -\t@echo " man to make manual pages" -\t@echo " texinfo to make Texinfo files" -\t@echo " info to make Texinfo files and run them through makeinfo" -\t@echo " gettext to make PO message catalogs" -\t@echo " changes to make an overview of all changed/added/deprecated items" -\t@echo " xml to make Docutils-native XML files" -\t@echo " pseudoxml to make pseudoxml-XML files for display purposes" -\t@echo " linkcheck to check all external links for integrity" -\t@echo " doctest to run all doctests embedded in the documentation \ +\t@echo " html to make standalone HTML files" +\t@echo " dirhtml to make HTML files named index.html in directories" +\t@echo " singlehtml to make a single large HTML file" +\t@echo " pickle to make pickle files" +\t@echo " json to make JSON files" +\t@echo " htmlhelp to make HTML files and a HTML help project" +\t@echo " qthelp to make HTML files and a qthelp project" +\t@echo " applehelp to make an Apple Help Book" +\t@echo " devhelp to make HTML files and a Devhelp project" +\t@echo " epub to make an epub" +\t@echo " epub3 to make an epub3" +\t@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" +\t@echo " latexpdf to make LaTeX files and run them through pdflatex" +\t@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" +\t@echo " lualatexpdf to make LaTeX files and run them through pdflatex" +\t@echo " xelatexpdf to make LaTeX files and run them through pdflatex" +\t@echo " text to make text files" +\t@echo " man to make manual pages" +\t@echo " texinfo to make Texinfo files" +\t@echo " info to make Texinfo files and run them through makeinfo" +\t@echo " gettext to make PO message catalogs" +\t@echo " changes to make an overview of all changed/added/deprecated items" +\t@echo " xml to make Docutils-native XML files" +\t@echo " pseudoxml to make pseudoxml-XML files for display purposes" +\t@echo " linkcheck to check all external links for integrity" +\t@echo " doctest to run all doctests embedded in the documentation \ (if enabled)" -\t@echo " coverage to run coverage check of the documentation (if enabled)" -\t@echo " dummy to check syntax errors of document sources" +\t@echo " coverage to run coverage check of the documentation (if enabled)" +\t@echo " dummy to check syntax errors of document sources" .PHONY: clean clean: @@ -678,6 +680,20 @@ latexpdfja: \t$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja \t@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." +.PHONY: lualatexpdf +lualatexpdf: +\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex +\t@echo "Running LaTeX files through lualatex..." +\t$(MAKE) PDFLATEX=lualatex -C $(BUILDDIR)/latex all-pdf +\t@echo "lualatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: xelatexpdf +xelatexpdf: +\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex +\t@echo "Running LaTeX files through xelatex..." +\t$(MAKE) PDFLATEX=xelatex -C $(BUILDDIR)/latex all-pdf +\t@echo "xelatex finished; the PDF files are in $(BUILDDIR)/latex." + .PHONY: text text: \t$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @@ -1451,11 +1467,8 @@ def generate(d, overwrite=True, silent=False): def write_file(fpath, content, newline=None): if overwrite or not path.isfile(fpath): print('Creating file %s.' % fpath) - f = open(fpath, 'wt', encoding='utf-8', newline=newline) - try: + with open(fpath, 'wt', encoding='utf-8', newline=newline) as f: f.write(content) - finally: - f.close() else: print('File %s already exists, skipping.' % fpath) diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 13fb4d528..2ba0c3802 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -15,6 +15,7 @@ from six.moves import cPickle as pickle from docutils.nodes import raw, comment, title, Text, NodeVisitor, SkipNode from os import path +import sphinx from sphinx.util import jsdump, rpartition from sphinx.util.pycompat import htmlescape @@ -180,6 +181,16 @@ class WordCollector(NodeVisitor): self.found_title_words = [] self.lang = lang + def is_meta_keywords(self, node, nodetype): + if isinstance(node, sphinx.addnodes.meta) and node.get('name') == 'keywords': + meta_lang = node.get('lang') + if meta_lang is None: # lang not specified + return True + elif meta_lang == self.lang.lang: # matched to html_search_language + return True + + return False + def dispatch_visit(self, node): nodetype = type(node) if issubclass(nodetype, comment): @@ -197,6 +208,10 @@ class WordCollector(NodeVisitor): self.found_words.extend(self.lang.split(node.astext())) elif issubclass(nodetype, title): self.found_title_words.extend(self.lang.split(node.astext())) + elif self.is_meta_keywords(node, nodetype): + keywords = node['content'] + keywords = [keyword.strip() for keyword in keywords.split(',')] + self.found_words.extend(keywords) class IndexBuilder(object): @@ -353,7 +368,6 @@ class IndexBuilder(object): def feed(self, filename, title, doctree): """Feed a doctree to the index.""" self._titles[filename] = title - visitor = WordCollector(doctree, self.lang) doctree.walk(visitor) diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty index 38ec46d86..956924e99 100644 --- a/sphinx/texinputs/sphinx.sty +++ b/sphinx/texinputs/sphinx.sty @@ -11,8 +11,6 @@ \@ifclassloaded{memoir}{}{\RequirePackage{fancyhdr}} \RequirePackage{textcomp} -% fancybox not used anymore and will be removed at Sphinx-1.5 -\RequirePackage{fancybox} \RequirePackage{titlesec} \RequirePackage{tabulary} \RequirePackage{makeidx} diff --git a/sphinx/util/png.py b/sphinx/util/png.py index e28445a42..476d45ccd 100644 --- a/sphinx/util/png.py +++ b/sphinx/util/png.py @@ -23,18 +23,14 @@ IEND_CHUNK = b'\x00\x00\x00\x00IEND\xAE\x42\x60\x82' def read_png_depth(filename): """Read the special tEXt chunk indicating the depth from a PNG file.""" - result = None - f = open(filename, 'rb') - try: + with open(filename, 'rb') as f: f.seek(- (LEN_IEND + LEN_DEPTH), 2) depthchunk = f.read(LEN_DEPTH) if not depthchunk.startswith(DEPTH_CHUNK_LEN + DEPTH_CHUNK_START): # either not a PNG file or not containing the depth chunk return None - result = struct.unpack('!i', depthchunk[14:18])[0] - finally: - f.close() - return result + else: + return struct.unpack('!i', depthchunk[14:18])[0] def write_png_depth(filename, depth): @@ -43,8 +39,7 @@ def write_png_depth(filename, depth): The chunk is placed immediately before the special IEND chunk. """ data = struct.pack('!i', depth) - f = open(filename, 'r+b') - try: + with open(filename, 'r+b') as f: # seek to the beginning of the IEND chunk f.seek(-LEN_IEND, 2) # overwrite it with the depth chunk @@ -54,5 +49,3 @@ def write_png_depth(filename, depth): f.write(struct.pack('!I', crc)) # replace the IEND chunk f.write(IEND_CHUNK) - finally: - f.close() diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index 0daa87981..c9474af9b 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -105,11 +105,8 @@ def execfile_(filepath, _globals, open=open): from sphinx.util.osutil import fs_encoding # get config source -- 'b' is a no-op under 2.x, while 'U' is # ignored under 3.x (but 3.x compile() accepts \r\n newlines) - f = open(filepath, 'rbU') - try: + with open(filepath, 'rbU') as f: source = f.read() - finally: - f.close() # py26 accept only LF eol instead of CRLF if sys.version_info[:2] == (2, 6): diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 606d549a6..69914da95 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -130,11 +130,8 @@ class WebSupport(object): """Load and return the "global context" pickle.""" if not self._globalcontext: infilename = path.join(self.datadir, 'globalcontext.pickle') - f = open(infilename, 'rb') - try: + with open(infilename, 'rb') as f: self._globalcontext = pickle.load(f) - finally: - f.close() return self._globalcontext def get_document(self, docname, username='', moderator=False): @@ -185,14 +182,11 @@ class WebSupport(object): infilename = docpath + '.fpickle' try: - f = open(infilename, 'rb') + with open(infilename, 'rb') as f: + document = pickle.load(f) except IOError: raise errors.DocumentNotFoundError( 'The document "%s" could not be found' % docname) - try: - document = pickle.load(f) - finally: - f.close() comment_opts = self._make_comment_options(username, moderator) comment_meta = self._make_metadata( diff --git a/test-reqs.txt b/test-reqs.txt index 0edb725dc..582afe69f 100644 --- a/test-reqs.txt +++ b/test-reqs.txt @@ -12,3 +12,4 @@ whoosh>=2.0 alabaster sphinx_rtd_theme imagesize +requests diff --git a/tests/roots/test-ext-inheritance_diagram/index.rst b/tests/roots/test-ext-inheritance_diagram/index.rst index 876996ca8..777192bd7 100644 --- a/tests/roots/test-ext-inheritance_diagram/index.rst +++ b/tests/roots/test-ext-inheritance_diagram/index.rst @@ -3,3 +3,6 @@ test-ext-inheritance_diagram ============================ .. inheritance-diagram:: test.Foo + +.. inheritance-diagram:: test.Foo + :caption: Test Foo! diff --git a/tests/roots/test-search/conf.py b/tests/roots/test-search/conf.py new file mode 100644 index 000000000..38b8b28c5 --- /dev/null +++ b/tests/roots/test-search/conf.py @@ -0,0 +1,3 @@ +master_doc = 'index' +exclude_patterns = ['_build'] +html_search_language = 'en' diff --git a/tests/roots/test-search/index.rst b/tests/roots/test-search/index.rst new file mode 100644 index 000000000..f2536ce9a --- /dev/null +++ b/tests/roots/test-search/index.rst @@ -0,0 +1,8 @@ +meta keywords +============= + +.. meta:: + :keywords lang=en: findthiskey, thistoo, notgerman + :keywords: thisonetoo + :keywords lang=de: onlygerman, onlytoogerman + :description: thisnoteither
\ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py index c05222601..d01d6afb4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -38,7 +38,7 @@ def test_core_config(app, status, warning): # simple default values assert 'locale_dirs' not in cfg.__dict__ - assert cfg.locale_dirs == [] + assert cfg.locale_dirs == ['locales'] assert cfg.trim_footnote_reference_space is False # complex default values diff --git a/tests/test_ext_inheritance_diagram.py b/tests/test_ext_inheritance_diagram.py index 64446eed8..bf1bbbac0 100644 --- a/tests/test_ext_inheritance_diagram.py +++ b/tests/test_ext_inheritance_diagram.py @@ -9,9 +9,34 @@ :license: BSD, see LICENSE for details. """ +import re from util import with_app +from test_ext_graphviz import skip_if_graphviz_not_found @with_app('html', testroot='ext-inheritance_diagram') +@skip_if_graphviz_not_found def test_inheritance_diagram_html(app, status, warning): app.builder.build_all() + + content = (app.outdir / 'index.html').text() + + pattern = ('<div class="figure" id="id1">\n' + '<img src="_images/inheritance-\w+.png" alt="Inheritance diagram of test.Foo" ' + 'class="inheritance"/>\n<p class="caption"><span class="caption-text">' + 'Test Foo!</span><a class="headerlink" href="#id1" ' + 'title="Permalink to this image">\xb6</a></p>') + assert re.search(pattern, content, re.M) + + +@with_app('latex', testroot='ext-inheritance_diagram') +@skip_if_graphviz_not_found +def test_inheritance_diagram_latex(app, status, warning): + app.builder.build_all() + + content = (app.outdir / 'Python.tex').text() + + pattern = ('\\\\begin{figure}\\[htbp]\n\\\\centering\n\\\\capstart\n\n' + '\\\\includegraphics{inheritance-\\w+.pdf}\n' + '\\\\caption{Test Foo!}\\\\label{index:id1}\\\\end{figure}') + assert re.search(pattern, content, re.M) diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py index 17c1a7a56..ece55ebe4 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_ext_napoleon_docstring.py @@ -249,7 +249,11 @@ class GoogleDocstringTest(BaseDocstringTest): )] def test_docstrings(self): - config = Config(napoleon_use_param=False, napoleon_use_rtype=False) + config = Config( + napoleon_use_param=False, + napoleon_use_rtype=False, + napoleon_use_keyword=False + ) for docstring, expected in self.docstrings: actual = str(GoogleDocstring(dedent(docstring), config)) expected = dedent(expected) @@ -1046,7 +1050,10 @@ class NumpyDocstringTest(BaseDocstringTest): )] def test_docstrings(self): - config = Config(napoleon_use_param=False, napoleon_use_rtype=False) + config = Config( + napoleon_use_param=False, + napoleon_use_rtype=False, + napoleon_use_keyword=False) for docstring, expected in self.docstrings: actual = str(NumpyDocstring(dedent(docstring), config)) expected = dedent(expected) @@ -1736,3 +1743,19 @@ definition_after_normal_text : int config = Config(napoleon_use_param=False) actual = str(NumpyDocstring(docstring, config)) self.assertEqual(expected, actual) + + def test_keywords_with_types(self): + docstring = """\ +Do as you please + +Keyword Args: + gotham_is_yours (None): shall interfere. +""" + actual = str(GoogleDocstring(docstring)) + expected = """\ +Do as you please + +:keyword gotham_is_yours: shall interfere. +:kwtype gotham_is_yours: None +""" + self.assertEqual(expected, actual) diff --git a/tests/test_search.py b/tests/test_search.py index cd2ff76f2..212ce778c 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -8,6 +8,7 @@ :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ +import os from docutils import frontend, utils from docutils.parsers import rst @@ -27,6 +28,17 @@ def setup_module(): parser = rst.Parser() +def jsload(path): + searchindex = path.text() + assert searchindex.startswith('Search.setIndex(') + + return jsdump.loads(searchindex[16:-2]) + + +def is_registered_term(index, keyword): + return index['terms'].get(keyword, []) != [] + + FILE_CONTENTS = '''\ .. test that comments are not indexed: boson @@ -52,3 +64,29 @@ def test_objects_are_escaped(app, status, warning): index = jsdump.loads(searchindex[16:-2]) assert 'n::Array<T, d>' in index.get('objects').get('') # n::Array<T,d> is escaped + + +@with_app(testroot='search') +def test_meta_keys_are_handled_for_language_en(app, status, warning): + app.builder.build_all() + searchindex = jsload(app.outdir / 'searchindex.js') + assert not is_registered_term(searchindex, 'thisnoteith') + assert is_registered_term(searchindex, 'thisonetoo') + assert is_registered_term(searchindex, 'findthiskei') + assert is_registered_term(searchindex, 'thistoo') + assert not is_registered_term(searchindex, 'onlygerman') + assert is_registered_term(searchindex, 'notgerman') + assert not is_registered_term(searchindex, 'onlytoogerman') + + +@with_app(testroot='search', confoverrides={'html_search_language': 'de'}) +def test_meta_keys_are_handled_for_language_de(app, status, warning): + app.builder.build_all() + searchindex = jsload(app.outdir / 'searchindex.js') + assert not is_registered_term(searchindex, 'thisnoteith') + assert is_registered_term(searchindex, 'thisonetoo') + assert not is_registered_term(searchindex, 'findthiskei') + assert not is_registered_term(searchindex, 'thistoo') + assert is_registered_term(searchindex, 'onlygerman') + assert not is_registered_term(searchindex, 'notgerman') + assert is_registered_term(searchindex, 'onlytoogerman') |
