diff options
Diffstat (limited to 'sphinx/builders')
-rw-r--r-- | sphinx/builders/__init__.py | 27 | ||||
-rw-r--r-- | sphinx/builders/applehelp.py | 57 | ||||
-rw-r--r-- | sphinx/builders/changes.py | 47 | ||||
-rw-r--r-- | sphinx/builders/devhelp.py | 32 | ||||
-rw-r--r-- | sphinx/builders/dummy.py | 4 | ||||
-rw-r--r-- | sphinx/builders/epub.py | 69 | ||||
-rw-r--r-- | sphinx/builders/epub3.py | 87 | ||||
-rw-r--r-- | sphinx/builders/gettext.py | 26 | ||||
-rw-r--r-- | sphinx/builders/html.py | 180 | ||||
-rw-r--r-- | sphinx/builders/htmlhelp.py | 36 | ||||
-rw-r--r-- | sphinx/builders/latex.py | 140 | ||||
-rw-r--r-- | sphinx/builders/linkcheck.py | 139 | ||||
-rw-r--r-- | sphinx/builders/manpage.py | 11 | ||||
-rw-r--r-- | sphinx/builders/qthelp.py | 29 | ||||
-rw-r--r-- | sphinx/builders/texinfo.py | 24 | ||||
-rw-r--r-- | sphinx/builders/text.py | 12 | ||||
-rw-r--r-- | sphinx/builders/websupport.py | 4 | ||||
-rw-r--r-- | sphinx/builders/xml.py | 12 |
18 files changed, 601 insertions, 335 deletions
diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index d0b6c2ca0..fe0c9c665 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -451,30 +451,3 @@ class Builder(object): except AttributeError: optname = '%s_%s' % (default, option) return getattr(self.config, optname) - - -BUILTIN_BUILDERS = { - 'dummy': ('dummy', 'DummyBuilder'), - 'html': ('html', 'StandaloneHTMLBuilder'), - 'dirhtml': ('html', 'DirectoryHTMLBuilder'), - 'singlehtml': ('html', 'SingleFileHTMLBuilder'), - 'pickle': ('html', 'PickleHTMLBuilder'), - 'json': ('html', 'JSONHTMLBuilder'), - 'web': ('html', 'PickleHTMLBuilder'), - 'htmlhelp': ('htmlhelp', 'HTMLHelpBuilder'), - 'devhelp': ('devhelp', 'DevhelpBuilder'), - 'qthelp': ('qthelp', 'QtHelpBuilder'), - 'applehelp': ('applehelp', 'AppleHelpBuilder'), - 'epub': ('epub', 'EpubBuilder'), - 'epub3': ('epub3', 'Epub3Builder'), - 'latex': ('latex', 'LaTeXBuilder'), - 'text': ('text', 'TextBuilder'), - 'man': ('manpage', 'ManualPageBuilder'), - 'texinfo': ('texinfo', 'TexinfoBuilder'), - 'changes': ('changes', 'ChangesBuilder'), - 'linkcheck': ('linkcheck', 'CheckExternalLinksBuilder'), - 'websupport': ('websupport', 'WebSupportBuilder'), - 'gettext': ('gettext', 'MessageCatalogBuilder'), - 'xml': ('xml', 'XMLBuilder'), - 'pseudoxml': ('xml', 'PseudoXMLBuilder'), -} diff --git a/sphinx/builders/applehelp.py b/sphinx/builders/applehelp.py index a6c89b628..7db086953 100644 --- a/sphinx/builders/applehelp.py +++ b/sphinx/builders/applehelp.py @@ -13,14 +13,16 @@ from __future__ import print_function import codecs import pipes -from os import path +from os import path, environ +import shlex from sphinx.builders.html import StandaloneHTMLBuilder -from sphinx.util import copy_static_entry -from sphinx.util.osutil import copyfile, ensuredir +from sphinx.config import string_classes +from sphinx.util.osutil import copyfile, ensuredir, make_filename from sphinx.util.console import bold +from sphinx.util.fileutil import copy_asset from sphinx.util.pycompat import htmlescape -from sphinx.util.matching import compile_matchers +from sphinx.util.matching import Matcher from sphinx.errors import SphinxError import plistlib @@ -105,17 +107,15 @@ class AppleHelpBuilder(StandaloneHTMLBuilder): self.finish_tasks.add_task(self.build_helpbook) def copy_localized_files(self): - source_dir = path.join(self.confdir, - self.config.applehelp_locale + '.lproj') + source_dir = path.join(self.confdir, self.config.applehelp_locale + '.lproj') target_dir = self.outdir if path.isdir(source_dir): self.info(bold('copying localized files... '), nonl=True) - ctx = self.globalcontext.copy() - matchers = compile_matchers(self.config.exclude_patterns) - copy_static_entry(source_dir, target_dir, self, ctx, - exclude_matchers=matchers) + excluded = Matcher(self.config.exclude_patterns + ['**/.*']) + copy_asset(source_dir, target_dir, excluded, + context=self.globalcontext, renderer=self.templates) self.info('done') @@ -179,14 +179,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 @@ -264,3 +261,35 @@ class AppleHelpBuilder(StandaloneHTMLBuilder): self.info('done') except OSError: raise AppleHelpCodeSigningFailed('Command not found: %s' % args[0]) + + +def setup(app): + app.setup_extension('sphinx.builders.html') + app.add_builder(AppleHelpBuilder) + + app.add_config_value('applehelp_bundle_name', + lambda self: make_filename(self.project), 'applehelp') + app.add_config_value('applehelp_bundle_id', None, 'applehelp', string_classes) + app.add_config_value('applehelp_dev_region', 'en-us', 'applehelp') + app.add_config_value('applehelp_bundle_version', '1', 'applehelp') + app.add_config_value('applehelp_icon', None, 'applehelp', string_classes) + app.add_config_value('applehelp_kb_product', + lambda self: '%s-%s' % (make_filename(self.project), self.release), + 'applehelp') + app.add_config_value('applehelp_kb_url', None, 'applehelp', string_classes) + app.add_config_value('applehelp_remote_url', None, 'applehelp', string_classes) + app.add_config_value('applehelp_index_anchors', False, 'applehelp', string_classes) + app.add_config_value('applehelp_min_term_length', None, 'applehelp', string_classes) + app.add_config_value('applehelp_stopwords', + lambda self: self.language or 'en', 'applehelp') + app.add_config_value('applehelp_locale', lambda self: self.language or 'en', 'applehelp') + app.add_config_value('applehelp_title', lambda self: self.project + ' Help', 'applehelp') + app.add_config_value('applehelp_codesign_identity', + lambda self: environ.get('CODE_SIGN_IDENTITY', None), + 'applehelp'), + app.add_config_value('applehelp_codesign_flags', + lambda self: shlex.split(environ.get('OTHER_CODE_SIGN_FLAGS', '')), + 'applehelp'), + app.add_config_value('applehelp_indexer_path', '/usr/bin/hiutil', 'applehelp') + app.add_config_value('applehelp_codesign_path', '/usr/bin/codesign', 'applehelp') + app.add_config_value('applehelp_disable_external_tools', False, None) diff --git a/sphinx/builders/changes.py b/sphinx/builders/changes.py index c077b7dd2..1bccb67d9 100644 --- a/sphinx/builders/changes.py +++ b/sphinx/builders/changes.py @@ -15,12 +15,12 @@ from os import path from six import iteritems from sphinx import package_dir -from sphinx.util import copy_static_entry from sphinx.locale import _ from sphinx.theming import Theme from sphinx.builders import Builder from sphinx.util.osutil import ensuredir, os_path from sphinx.util.console import bold +from sphinx.util.fileutil import copy_asset_file from sphinx.util.pycompat import htmlescape @@ -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,35 +120,28 @@ 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', - 'static', 'default.css_t'), - self.outdir, self, themectx) - copy_static_entry(path.join(package_dir, 'themes', 'basic', - 'static', 'basic.css'), - self.outdir, self) + copy_asset_file(path.join(package_dir, 'themes', 'default', 'static', 'default.css_t'), + self.outdir, context=themectx, renderer=self.templates) + copy_asset_file(path.join(package_dir, 'themes', 'basic', 'static', 'basic.css'), + self.outdir) def hl(self, text, version): text = htmlescape(text) @@ -165,3 +152,7 @@ class ChangesBuilder(Builder): def finish(self): pass + + +def setup(app): + app.add_builder(ChangesBuilder) diff --git a/sphinx/builders/devhelp.py b/sphinx/builders/devhelp.py index 523941110..fd6f3400e 100644 --- a/sphinx/builders/devhelp.py +++ b/sphinx/builders/devhelp.py @@ -13,32 +13,19 @@ from __future__ import absolute_import import re +import gzip from os import path from docutils import nodes from sphinx import addnodes +from sphinx.util.osutil import make_filename from sphinx.builders.html import StandaloneHTMLBuilder try: import xml.etree.ElementTree as etree except ImportError: - try: - import lxml.etree as etree - except ImportError: - try: - import elementtree.ElementTree as etree - except ImportError: - import cElementTree as etree - -try: - import gzip - - def comp_open(filename, mode='rb'): - return gzip.open(filename + '.gz', mode) -except ImportError: - def comp_open(filename, mode='rb'): - return open(filename, mode) + import lxml.etree as etree class DevhelpBuilder(StandaloneHTMLBuilder): @@ -128,8 +115,13 @@ class DevhelpBuilder(StandaloneHTMLBuilder): write_index(title, refs, subitems) # Dump the XML file - f = comp_open(path.join(outdir, outname + '.devhelp'), 'w') - try: + xmlfile = path.join(outdir, outname + '.devhelp.gz') + with gzip.open(xmlfile, 'w') as f: tree.write(f, 'utf-8') - finally: - f.close() + + +def setup(app): + app.setup_extension('sphinx.builders.html') + app.add_builder(DevhelpBuilder) + + app.add_config_value('devhelp_basename', lambda self: make_filename(self.project), None) diff --git a/sphinx/builders/dummy.py b/sphinx/builders/dummy.py index 75b834c2b..b119d9687 100644 --- a/sphinx/builders/dummy.py +++ b/sphinx/builders/dummy.py @@ -34,3 +34,7 @@ class DummyBuilder(Builder): def finish(self): pass + + +def setup(app): + app.add_builder(DummyBuilder) diff --git a/sphinx/builders/epub.py b/sphinx/builders/epub.py index ade887e56..b4b657468 100644 --- a/sphinx/builders/epub.py +++ b/sphinx/builders/epub.py @@ -29,7 +29,7 @@ from docutils import nodes from sphinx import addnodes from sphinx.builders.html import StandaloneHTMLBuilder -from sphinx.util.osutil import ensuredir, copyfile, EEXIST +from sphinx.util.osutil import ensuredir, copyfile, make_filename, EEXIST from sphinx.util.smartypants import sphinx_smarty_pants as ssp from sphinx.util.console import brown @@ -179,7 +179,7 @@ class EpubBuilder(StandaloneHTMLBuilder): META-INF/container.xml. Afterwards, all necessary files are zipped to an epub file. """ - name = 'epub' + name = 'epub2' # don't copy the reST source copysource = False @@ -188,8 +188,12 @@ class EpubBuilder(StandaloneHTMLBuilder): # don't add links add_permalinks = False + # don't use # as current path. ePub check reject it. + allow_sharp_as_current_path = False # don't add sidebar etc. embedded = True + # disable download role + download_support = False # dont' create links to original images from images html_scaled_image_link = False # don't generate search index or include search page @@ -223,6 +227,7 @@ class EpubBuilder(StandaloneHTMLBuilder): self.link_suffix = '.xhtml' self.playorder = 0 self.tocid = 0 + self.use_index = self.get_builder_config('use_index', 'epub') def get_theme_config(self): return self.config.epub_theme, self.config.epub_theme_options @@ -485,6 +490,9 @@ class EpubBuilder(StandaloneHTMLBuilder): else: super(EpubBuilder, self).copy_image_files() + def copy_download_files(self): + pass + def handle_page(self, pagename, addctx, templatename='page.html', outfilename=None, event_arg=None): """Create a rendered page. @@ -493,6 +501,8 @@ class EpubBuilder(StandaloneHTMLBuilder): attributes. """ if pagename.startswith('genindex'): + if not self.use_index: + return self.fix_genindex(addctx['genindexentries']) addctx['doctype'] = self.doctype StandaloneHTMLBuilder.handle_page(self, pagename, addctx, templatename, @@ -511,11 +521,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.""" @@ -526,11 +533,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 @@ -565,8 +569,11 @@ class EpubBuilder(StandaloneHTMLBuilder): self.files = [] self.ignored_files = ['.buildinfo', 'mimetype', 'content.opf', 'toc.ncx', 'META-INF/container.xml', + 'Thumbs.db', 'ehthumbs.db', '.DS_Store', self.config.epub_basename + '.epub'] + \ self.config.epub_exclude_files + if not self.use_index: + self.ignored_files.append('genindex' + self.out_suffix) for root, dirs, files in os.walk(outdir): for fn in files: filename = path.join(root, fn)[olen:] @@ -604,7 +611,7 @@ class EpubBuilder(StandaloneHTMLBuilder): 'idref': self.esc(self.make_id(info[0] + self.out_suffix)) }) spinefiles.add(info[0] + self.out_suffix) - if self.get_builder_config('use_index', 'epub'): + if self.use_index: spine.append(self.spine_template % { 'idref': self.esc(self.make_id('genindex' + self.out_suffix)) }) @@ -677,12 +684,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.""" @@ -774,11 +778,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. @@ -797,3 +798,33 @@ class EpubBuilder(StandaloneHTMLBuilder): fp = path.join(outdir, file) epub.write(fp, file, zipfile.ZIP_DEFLATED) epub.close() + + +def setup(app): + app.setup_extension('sphinx.builders.html') + app.add_builder(EpubBuilder) + + # config values + app.add_config_value('epub_basename', lambda self: make_filename(self.project), None) + app.add_config_value('epub_theme', 'epub', 'html') + app.add_config_value('epub_theme_options', {}, 'html') + app.add_config_value('epub_title', lambda self: self.html_title, 'html') + app.add_config_value('epub_author', 'unknown', 'html') + app.add_config_value('epub_language', lambda self: self.language or 'en', 'html') + app.add_config_value('epub_publisher', 'unknown', 'html') + app.add_config_value('epub_copyright', lambda self: self.copyright, 'html') + app.add_config_value('epub_identifier', 'unknown', 'html') + app.add_config_value('epub_scheme', 'unknown', 'html') + app.add_config_value('epub_uid', 'unknown', 'env') + app.add_config_value('epub_cover', (), 'env') + app.add_config_value('epub_guide', (), 'env') + app.add_config_value('epub_pre_files', [], 'env') + app.add_config_value('epub_post_files', [], 'env') + app.add_config_value('epub_exclude_files', [], 'env') + app.add_config_value('epub_tocdepth', 3, 'env') + app.add_config_value('epub_tocdup', True, 'env') + app.add_config_value('epub_tocscope', 'default', 'env') + app.add_config_value('epub_fix_images', False, 'env') + app.add_config_value('epub_max_image_width', 0, 'env') + app.add_config_value('epub_show_urls', 'inline', 'html') + app.add_config_value('epub_use_index', lambda self: self.html_use_index, 'html') diff --git a/sphinx/builders/epub3.py b/sphinx/builders/epub3.py index d792bd6e9..ae799986e 100644 --- a/sphinx/builders/epub3.py +++ b/sphinx/builders/epub3.py @@ -14,6 +14,7 @@ import codecs from os import path from datetime import datetime +from sphinx.config import string_classes from sphinx.builders.epub import EpubBuilder @@ -52,7 +53,8 @@ NAVLIST_INDENT = ' ' PACKAGE_DOC_TEMPLATE = u'''\ <?xml version="1.0" encoding="UTF-8"?> <package xmlns="http://www.idpf.org/2007/opf" version="3.0" xml:lang="%(lang)s" - unique-identifier="%(uid)s"> + unique-identifier="%(uid)s" + prefix="ibooks: http://vocabulary.itunes.apple.com/rdf/ibooks/vocabulary-extensions-1.0/"> <metadata xmlns:opf="http://www.idpf.org/2007/opf" xmlns:dc="http://purl.org/dc/elements/1.1/"> <dc:language>%(lang)s</dc:language> @@ -65,6 +67,10 @@ PACKAGE_DOC_TEMPLATE = u'''\ <dc:identifier id="%(uid)s">%(id)s</dc:identifier> <dc:date>%(date)s</dc:date> <meta property="dcterms:modified">%(date)s</meta> + <meta property="ibooks:version">%(version)s</meta> + <meta property="ibooks:specified-fonts">true</meta> + <meta property="ibooks:binding">true</meta> + <meta property="ibooks:scroll-axis">%(ibook_scroll_axis)s</meta> </metadata> <manifest> <item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml" /> @@ -94,7 +100,7 @@ class Epub3Builder(EpubBuilder): and META-INF/container.xml. Afterwards, all necessary files are zipped to an epub file. """ - name = 'epub3' + name = 'epub' navigation_doc_template = NAVIGATION_DOC_TEMPLATE navlist_template = NAVLIST_TEMPLATE @@ -122,13 +128,43 @@ class Epub3Builder(EpubBuilder): """ metadata = super(Epub3Builder, self).content_metadata( files, spine, guide) - metadata['description'] = self.esc(self.config.epub3_description) - metadata['contributor'] = self.esc(self.config.epub3_contributor) - metadata['page_progression_direction'] = self.esc( - self.config.epub3_page_progression_direction) or 'default' + metadata['description'] = self.esc(self.config.epub_description) + metadata['contributor'] = self.esc(self.config.epub_contributor) + metadata['page_progression_direction'] = self._page_progression_direction() + metadata['ibook_scroll_axis'] = self._ibook_scroll_axis() metadata['date'] = self.esc(datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")) + metadata['version'] = self.esc(self.config.version) return metadata + def _page_progression_direction(self): + if self.config.epub_writing_mode == 'horizontal': + page_progression_direction = 'ltr' + elif self.config.epub_writing_mode == 'vertical': + page_progression_direction = 'rtl' + else: + page_progression_direction = 'default' + return page_progression_direction + + def _ibook_scroll_axis(self): + if self.config.epub_writing_mode == 'horizontal': + scroll_axis = 'vertical' + elif self.config.epub_writing_mode == 'vertical': + scroll_axis = 'horizontal' + else: + scroll_axis = 'default' + return scroll_axis + + def _css_writing_mode(self): + if self.config.epub_writing_mode == 'vertical': + editing_mode = 'vertical-rl' + else: + editing_mode = 'horizontal-tb' + return editing_mode + + def prepare_writing(self, docnames): + super(Epub3Builder, self).prepare_writing(docnames) + self.globalcontext['theme_writing_mode'] = self._css_writing_mode() + def new_navlist(self, node, level, has_child): """Create a new entry in the toc from the node at given level.""" # XXX Modifies the node @@ -211,12 +247,37 @@ 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 - if outname not in self.files: - self.files.append(outname) + + # Add nav.xhtml to epub file + if outname not in self.files: + self.files.append(outname) + + +def validate_config_values(app): + if app.config.epub3_description is not None: + app.warn('epub3_description is deprecated. Use epub_description instead.') + app.config.epub_description = app.config.epub3_description + + if app.config.epub3_contributor is not None: + app.warn('epub3_contributor is deprecated. Use epub_contributor instead.') + app.config.epub_contributor = app.config.epub3_contributor + + if app.config.epub3_page_progression_direction is not None: + app.warn('epub3_page_progression_direction option is deprecated' + ' from 1.5. Use epub_writing_mode instead.') + + +def setup(app): + app.setup_extension('sphinx.builders.epub') + app.add_builder(Epub3Builder) + app.connect('builder-inited', validate_config_values) + + app.add_config_value('epub_description', '', 'epub3', string_classes) + app.add_config_value('epub_contributor', 'unknown', 'epub3', string_classes) + app.add_config_value('epub_writing_mode', 'horizontal', 'epub3', string_classes) + app.add_config_value('epub3_description', None, 'epub3', string_classes) + app.add_config_value('epub3_contributor', None, 'epub3', string_classes) + app.add_config_value('epub3_page_progression_direction', None, 'epub3', string_classes) diff --git a/sphinx/builders/gettext.py b/sphinx/builders/gettext.py index 9edbdf1d3..e118cde99 100644 --- a/sphinx/builders/gettext.py +++ b/sphinx/builders/gettext.py @@ -22,6 +22,7 @@ from six import iteritems from sphinx.builders import Builder from sphinx.util import split_index_msg +from sphinx.util.tags import Tags from sphinx.util.nodes import extract_messages, traverse_translatable_index from sphinx.util.osutil import safe_relpath, ensuredir, canon_path from sphinx.util.i18n import find_catalog @@ -79,6 +80,16 @@ class MsgOrigin(object): self.uid = uuid4().hex +class I18nTags(Tags): + """Dummy tags module for I18nBuilder. + + To translate all text inside of only nodes, this class + always returns True value even if no tags are defined. + """ + def eval_condition(self, condition): + return True + + class I18nBuilder(Builder): """ General i18n builder. @@ -93,6 +104,7 @@ class I18nBuilder(Builder): def init(self): Builder.init(self) + self.tags = I18nTags() self.catalogs = defaultdict(Catalog) def get_target_uri(self, docname, typ=None): @@ -212,8 +224,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: @@ -236,5 +247,12 @@ class MessageCatalogBuilder(I18nBuilder): replace('\n', '\\n"\n"') pofile.write('msgid "%s"\nmsgstr ""\n\n' % message) - finally: - pofile.close() + +def setup(app): + app.add_builder(MessageCatalogBuilder) + + app.add_config_value('gettext_compact', True, 'gettext') + app.add_config_value('gettext_location', True, 'gettext') + app.add_config_value('gettext_uuid', False, 'gettext') + app.add_config_value('gettext_auto_build', True, 'env') + app.add_config_value('gettext_additional_targets', [], 'env') diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index f5d8cf549..e13d752d7 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -27,13 +27,15 @@ from docutils.frontend import OptionParser from docutils.readers.doctree import Reader as DoctreeReader from sphinx import package_dir, __display_version__ -from sphinx.util import jsonimpl, copy_static_entry, copy_extra_entry +from sphinx.util import jsonimpl from sphinx.util.i18n import format_date from sphinx.util.osutil import SEP, os_path, relative_uri, ensuredir, \ movefile, copyfile from sphinx.util.nodes import inline_all_toctrees -from sphinx.util.matching import patmatch, compile_matchers -from sphinx.locale import _ +from sphinx.util.fileutil import copy_asset +from sphinx.util.matching import patmatch, Matcher, DOTFILES +from sphinx.config import string_classes +from sphinx.locale import _, l_ from sphinx.search import js_index from sphinx.theming import Theme from sphinx.builders import Builder @@ -80,8 +82,10 @@ class StandaloneHTMLBuilder(Builder): 'image/gif', 'image/jpeg'] searchindex_filename = 'searchindex.js' add_permalinks = True + allow_sharp_as_current_path = True embedded = False # for things like HTML help or Qt help: suppresses sidebar search = True # for things like HTML help and Apple help: suppress search + download_support = True # enable download role # This is a class attribute because it is mutated by Sphinx.add_javascript. script_files = ['_static/jquery.js', '_static/underscore.js', @@ -120,6 +124,7 @@ class StandaloneHTMLBuilder(Builder): if self.config.language is not None: if self._get_translations_js(): self.script_files.append('_static/translations.js') + self.use_index = self.get_builder_config('use_index', 'html') def _get_translations_js(self): candidates = [path.join(package_dir, 'locale', self.config.language, @@ -158,16 +163,11 @@ class StandaloneHTMLBuilder(Builder): self.config.trim_doctest_flags) def init_translator_class(self): - if self.translator_class is not None: - pass - elif self.config.html_translator_class: - self.translator_class = self.app.import_object( - self.config.html_translator_class, - 'html_translator_class setting') - elif self.config.html_use_smartypants: - self.translator_class = SmartyPantsHTMLTranslator - else: - self.translator_class = HTMLTranslator + if self.translator_class is None: + if self.config.html_use_smartypants: + self.translator_class = SmartyPantsHTMLTranslator + else: + self.translator_class = HTMLTranslator def get_outdated_docs(self): cfgdict = dict((name, self.config[name]) @@ -177,8 +177,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 @@ -189,8 +188,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')) @@ -314,7 +311,7 @@ class StandaloneHTMLBuilder(Builder): self.relations = self.env.collect_relations() rellinks = [] - if self.get_builder_config('use_index', 'html'): + if self.use_index: rellinks.append(('genindex', _('General Index'), 'I', _('index'))) for indexname, indexcls, content, collapse in self.domain_indices: # if it has a short name @@ -344,6 +341,7 @@ class StandaloneHTMLBuilder(Builder): show_sphinx = self.config.html_show_sphinx, has_source = self.config.html_copy_source, show_source = self.config.html_show_sourcelink, + sourcelink_suffix = self.config.html_sourcelink_suffix, file_suffix = self.out_suffix, script_files = self.script_files, language = self.config.language, @@ -407,15 +405,21 @@ class StandaloneHTMLBuilder(Builder): # title rendered as HTML title = self.env.longtitles.get(docname) title = title and self.render_partial(title)['title'] or '' + + # Suffix for the document + source_suffix = path.splitext(self.env.doc2path(docname))[1] + # the name for the copied source - sourcename = self.config.html_copy_source and docname + '.txt' or '' + if self.config.html_copy_source: + sourcename = docname + source_suffix + if source_suffix != self.config.html_sourcelink_suffix: + sourcename += self.config.html_sourcelink_suffix + else: + sourcename = '' # metadata for the document meta = self.env.metadata.get(docname) - # Suffix for the document - source_suffix = '.' + self.env.doc2path(docname).split('.')[-1] - # local TOC and global TOC tree self_toc = self.env.get_toc_for(docname, self) toc = self.render_partial(self_toc)['fragment'] @@ -476,7 +480,7 @@ class StandaloneHTMLBuilder(Builder): self.info(bold('generating indices...'), nonl=1) # the global general index - if self.get_builder_config('use_index', 'html'): + if self.use_index: self.write_genindex() # the global domain-specific indices @@ -586,9 +590,8 @@ class StandaloneHTMLBuilder(Builder): self.info(bold('copying static files... '), nonl=True) ensuredir(path.join(self.outdir, '_static')) # first, create pygments style file - f = open(path.join(self.outdir, '_static', 'pygments.css'), 'w') - f.write(self.highlighter.get_stylesheet()) - f.close() + with open(path.join(self.outdir, '_static', 'pygments.css'), 'w') as f: + f.write(self.highlighter.get_stylesheet()) # then, copy translations JavaScript file if self.config.language is not None: jsfile = self._get_translations_js() @@ -610,21 +613,19 @@ class StandaloneHTMLBuilder(Builder): # then, copy over theme-supplied static files if self.theme: - themeentries = [path.join(themepath, 'static') - for themepath in self.theme.get_dirchain()[::-1]] - for entry in themeentries: - copy_static_entry(entry, path.join(self.outdir, '_static'), - self, ctx) + for theme_path in self.theme.get_dirchain()[::-1]: + entry = path.join(theme_path, 'static') + copy_asset(entry, path.join(self.outdir, '_static'), excluded=DOTFILES, + context=ctx, renderer=self.templates) # then, copy over all user-supplied static files - staticentries = [path.join(self.confdir, spath) - for spath in self.config.html_static_path] - matchers = compile_matchers(self.config.exclude_patterns) - for entry in staticentries: + excluded = Matcher(self.config.exclude_patterns + ["**/.*"]) + for static_path in self.config.html_static_path: + entry = path.join(self.confdir, static_path) if not path.exists(entry): self.warn('html_static_path entry %r does not exist' % entry) continue - copy_static_entry(entry, path.join(self.outdir, '_static'), self, - ctx, exclude_matchers=matchers) + copy_asset(entry, path.join(self.outdir, '_static'), excluded, + context=ctx, renderer=self.templates) # copy logo and favicon files if not already in static path if self.config.html_logo: logobase = path.basename(self.config.html_logo) @@ -647,27 +648,25 @@ class StandaloneHTMLBuilder(Builder): def copy_extra_files(self): # copy html_extra_path files self.info(bold('copying extra files... '), nonl=True) - extraentries = [path.join(self.confdir, epath) - for epath in self.config.html_extra_path] - matchers = compile_matchers(self.config.exclude_patterns) - for entry in extraentries: + excluded = Matcher(self.config.exclude_patterns) + + for extra_path in self.config.html_extra_path: + entry = path.join(self.confdir, extra_path) if not path.exists(entry): self.warn('html_extra_path entry %r does not exist' % entry) continue - copy_extra_entry(entry, self.outdir, matchers) + + copy_asset(entry, self.outdir, excluded) self.info('done') 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 @@ -707,10 +706,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 ' @@ -722,7 +719,12 @@ class StandaloneHTMLBuilder(Builder): def index_page(self, pagename, doctree, title): # only index pages with title if self.indexer is not None and title: - self.indexer.feed(pagename, title, doctree) + filename = self.env.doc2path(pagename, base=None) + try: + self.indexer.feed(pagename, filename, title, doctree) + except TypeError: + # fallback for old search-adapters + self.indexer.feed(pagename, title, doctree) def _get_local_toctree(self, docname, collapse=True, **kwds): if 'includehidden' not in kwds: @@ -785,6 +787,8 @@ class StandaloneHTMLBuilder(Builder): elif not resource: otheruri = self.get_target_uri(otheruri) uri = relative_uri(baseuri, otheruri) or '#' + if uri == '#' and not self.allow_sharp_as_current_path: + uri = baseuri return uri ctx['pathto'] = pathto @@ -824,11 +828,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'): @@ -845,8 +846,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' @@ -868,8 +868,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): @@ -884,10 +882,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') @@ -1106,7 +1102,9 @@ class SerializingHTMLBuilder(StandaloneHTMLBuilder): self.theme = None # no theme necessary self.templates = None # no template bridge necessary self.init_translator_class() + self.init_templates() self.init_highlighter() + self.use_index = self.get_builder_config('use_index', 'html') def get_target_uri(self, docname, typ=None): if docname == 'index': @@ -1120,10 +1118,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): @@ -1201,3 +1197,59 @@ class JSONHTMLBuilder(SerializingHTMLBuilder): def init(self): SerializingHTMLBuilder.init(self) + + +def validate_config_values(app): + if app.config.html_translator_class: + app.warn('html_translator_class is deprecated. ' + 'Use Sphinx.set_translator() API instead.') + + +def setup(app): + # builders + app.add_builder(StandaloneHTMLBuilder) + app.add_builder(DirectoryHTMLBuilder) + app.add_builder(SingleFileHTMLBuilder) + app.add_builder(PickleHTMLBuilder) + app.add_builder(JSONHTMLBuilder) + + app.connect('builder-inited', validate_config_values) + + # config values + app.add_config_value('html_theme', 'alabaster', 'html') + app.add_config_value('html_theme_path', [], 'html') + app.add_config_value('html_theme_options', {}, 'html') + app.add_config_value('html_title', + lambda self: l_('%s %s documentation') % (self.project, self.release), + 'html', string_classes) + app.add_config_value('html_short_title', lambda self: self.html_title, 'html') + app.add_config_value('html_style', None, 'html', string_classes) + app.add_config_value('html_logo', None, 'html', string_classes) + app.add_config_value('html_favicon', None, 'html', string_classes) + app.add_config_value('html_static_path', [], 'html') + app.add_config_value('html_extra_path', [], 'html') + app.add_config_value('html_last_updated_fmt', None, 'html', string_classes) + app.add_config_value('html_use_smartypants', True, 'html') + app.add_config_value('html_sidebars', {}, 'html') + app.add_config_value('html_additional_pages', {}, 'html') + app.add_config_value('html_use_modindex', True, 'html') # deprecated + app.add_config_value('html_domain_indices', True, 'html', [list]) + app.add_config_value('html_add_permalinks', u'\u00B6', 'html') + app.add_config_value('html_use_index', True, 'html') + app.add_config_value('html_split_index', False, 'html') + app.add_config_value('html_copy_source', True, 'html') + app.add_config_value('html_show_sourcelink', True, 'html') + app.add_config_value('html_sourcelink_suffix', '.txt', 'html') + app.add_config_value('html_use_opensearch', '', 'html') + app.add_config_value('html_file_suffix', None, 'html', string_classes) + app.add_config_value('html_link_suffix', None, 'html', string_classes) + app.add_config_value('html_show_copyright', True, 'html') + app.add_config_value('html_show_sphinx', True, 'html') + app.add_config_value('html_context', {}, 'html') + app.add_config_value('html_output_encoding', 'utf-8', 'html') + app.add_config_value('html_compact_lists', True, 'html') + app.add_config_value('html_secnumber_suffix', '. ', 'html') + app.add_config_value('html_search_language', None, 'html', string_classes) + app.add_config_value('html_search_options', {}, 'html') + app.add_config_value('html_search_scorer', '', None) + app.add_config_value('html_scaled_image_link', True, 'html') diff --git a/sphinx/builders/htmlhelp.py b/sphinx/builders/htmlhelp.py index f1626321e..ecc752b60 100644 --- a/sphinx/builders/htmlhelp.py +++ b/sphinx/builders/htmlhelp.py @@ -18,6 +18,7 @@ from os import path from docutils import nodes from sphinx import addnodes +from sphinx.util.osutil import make_filename from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.util.pycompat import htmlescape @@ -197,18 +198,22 @@ class HTMLHelpBuilder(StandaloneHTMLBuilder): def handle_finish(self): self.build_hhx(self.outdir, self.config.htmlhelp_basename) + def write_doc(self, docname, doctree): + for node in doctree.traverse(nodes.reference): + # add ``target=_blank`` attributes to external links + if node.get('internal') is None and 'refuri' in node: + node['target'] = '_blank' + + StandaloneHTMLBuilder.write_doc(self, docname, doctree) + 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, @@ -227,12 +232,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, @@ -270,13 +272,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): @@ -306,5 +305,10 @@ class HTMLHelpBuilder(StandaloneHTMLBuilder): for title, (refs, subitems, key_) in group: write_index(title, refs, subitems) f.write('</UL>\n') - finally: - f.close() + + +def setup(app): + app.setup_extension('sphinx.builders.html') + app.add_builder(HTMLHelpBuilder) + + app.add_config_value('htmlhelp_basename', lambda self: make_filename(self.project), None) diff --git a/sphinx/builders/latex.py b/sphinx/builders/latex.py index ac26e33c9..0ef0d70d4 100644 --- a/sphinx/builders/latex.py +++ b/sphinx/builders/latex.py @@ -11,7 +11,6 @@ import os from os import path -import warnings from six import iteritems from docutils import nodes @@ -19,14 +18,16 @@ from docutils.io import FileOutput from docutils.utils import new_document from docutils.frontend import OptionParser -from sphinx import package_dir, addnodes +from sphinx import package_dir, addnodes, highlighting from sphinx.util import texescape +from sphinx.config import string_classes, ENUM from sphinx.errors import SphinxError from sphinx.locale import _ from sphinx.builders import Builder from sphinx.environment import NoUri from sphinx.util.nodes import inline_all_toctrees -from sphinx.util.osutil import SEP, copyfile +from sphinx.util.fileutil import copy_asset_file +from sphinx.util.osutil import SEP, make_filename from sphinx.util.console import bold, darkgreen from sphinx.writers.latex import LaTeXWriter @@ -38,27 +39,12 @@ class LaTeXBuilder(Builder): name = 'latex' format = 'latex' supported_image_types = ['application/pdf', 'image/png', 'image/jpeg'] - usepackages = [] def init(self): self.docnames = [] self.document_data = [] + self.usepackages = [] texescape.init() - self.check_options() - - def check_options(self): - if self.config.latex_toplevel_sectioning not in (None, 'part', 'chapter', 'section'): - self.warn('invalid latex_toplevel_sectioning, ignored: %s' % - self.config.latex_top_sectionlevel) - self.config.latex_top_sectionlevel = None - - if self.config.latex_use_parts: - warnings.warn('latex_use_parts will be removed at Sphinx-1.5. ' - 'Use latex_toplevel_sectioning instead.', - DeprecationWarning) - - if self.config.latex_toplevel_sectioning: - self.warn('latex_use_parts conflicts with latex_toplevel_sectioning, ignored.') def get_outdated_docs(self): return 'all documents' # for now @@ -92,6 +78,16 @@ class LaTeXBuilder(Builder): docname = docname[:-5] self.titles.append((docname, entry[2])) + def write_stylesheet(self): + highlighter = highlighting.PygmentsBridge( + 'latex', self.config.pygments_style, self.config.trim_doctest_flags) + stylesheet = path.join(self.outdir, 'sphinxhighlight.sty') + with open(stylesheet, 'w') as f: + f.write('\\NeedsTeXFormat{LaTeX2e}[1995/12/01]\n') + f.write('\\ProvidesPackage{sphinxhighlight}' + '[2016/05/29 stylesheet for highlighting with pygments]\n\n') + f.write(highlighter.get_stylesheet()) + def write(self, *ignored): docwriter = LaTeXWriter(self) docsettings = OptionParser( @@ -100,6 +96,7 @@ class LaTeXBuilder(Builder): read_config_files=True).get_default_values() self.init_document_data() + self.write_stylesheet() for entry in self.document_data: docname, targetname, title, author, docclass = entry[:5] @@ -192,33 +189,118 @@ class LaTeXBuilder(Builder): self.info(bold('copying images...'), nonl=1) for src, dest in iteritems(self.images): self.info(' '+src, nonl=1) - copyfile(path.join(self.srcdir, src), - path.join(self.outdir, dest)) + copy_asset_file(path.join(self.srcdir, src), + path.join(self.outdir, dest)) self.info() # copy TeX support files from texinputs + context = {'latex_engine': self.config.latex_engine} self.info(bold('copying TeX support files...')) staticdirname = path.join(package_dir, 'texinputs') for filename in os.listdir(staticdirname): if not filename.startswith('.'): - copyfile(path.join(staticdirname, filename), - path.join(self.outdir, filename)) + copy_asset_file(path.join(staticdirname, filename), + self.outdir, context=context) # copy additional files if self.config.latex_additional_files: self.info(bold('copying additional files...'), nonl=1) for filename in self.config.latex_additional_files: self.info(' '+filename, nonl=1) - copyfile(path.join(self.confdir, filename), - path.join(self.outdir, path.basename(filename))) + copy_asset_file(path.join(self.confdir, filename), self.outdir) self.info() # the logo is handled differently if self.config.latex_logo: - logobase = path.basename(self.config.latex_logo) - logotarget = path.join(self.outdir, logobase) if not path.isfile(path.join(self.confdir, self.config.latex_logo)): raise SphinxError('logo file %r does not exist' % self.config.latex_logo) - elif not path.isfile(logotarget): - copyfile(path.join(self.confdir, self.config.latex_logo), logotarget) + else: + copy_asset_file(path.join(self.confdir, self.config.latex_logo), self.outdir) self.info('done') + + +def validate_config_values(app): + if app.config.latex_toplevel_sectioning not in (None, 'part', 'chapter', 'section'): + app.warn('invalid latex_toplevel_sectioning, ignored: %s' % + app.config.latex_toplevel_sectioning) + app.config.latex_toplevel_sectioning = None + + if app.config.latex_use_parts: + if app.config.latex_toplevel_sectioning: + app.warn('latex_use_parts conflicts with latex_toplevel_sectioning, ignored.') + else: + app.warn('latex_use_parts is deprecated. Use latex_toplevel_sectioning instead.') + app.config.latex_toplevel_sectioning = 'parts' + + if app.config.latex_use_modindex is not True: # changed by user + app.warn('latex_use_modeindex is deprecated. Use latex_domain_indices instead.') + + if app.config.latex_preamble: + if app.config.latex_elements.get('preamble'): + app.warn("latex_preamble conflicts with latex_elements['preamble'], ignored.") + else: + app.warn("latex_preamble is deprecated. Use latex_elements['preamble'] instead.") + app.config.latex_elements['preamble'] = app.config.latex_preamble + + if app.config.latex_paper_size != 'letter': + if app.config.latex_elements.get('papersize'): + app.warn("latex_paper_size conflicts with latex_elements['papersize'], ignored.") + else: + app.warn("latex_paper_size is deprecated. " + "Use latex_elements['papersize'] instead.") + if app.config.latex_paper_size: + app.config.latex_elements['papersize'] = app.config.latex_paper_size + 'paper' + + if app.config.latex_font_size != '10pt': + if app.config.latex_elements.get('pointsize'): + app.warn("latex_font_size conflicts with latex_elements['pointsize'], ignored.") + else: + app.warn("latex_font_size is deprecated. Use latex_elements['pointsize'] instead.") + app.config.latex_elements['pointsize'] = app.config.latex_font_size + + if 'footer' in app.config.latex_elements: + if 'postamble' in app.config.latex_elements: + app.warn("latex_elements['footer'] conflicts with " + "latex_elements['postamble'], ignored.") + else: + app.warn("latex_elements['footer'] is deprecated. " + "Use latex_elements['preamble'] instead.") + app.config.latex_elements['postamble'] = app.config.latex_elements['footer'] + + +def setup(app): + app.add_builder(LaTeXBuilder) + app.connect('builder-inited', validate_config_values) + + app.add_config_value('latex_engine', + lambda self: 'pdflatex' if self.language != 'ja' else 'platex', + None, + ENUM('pdflatex', 'xelatex', 'lualatex', 'platex')) + app.add_config_value('latex_documents', + lambda self: [(self.master_doc, make_filename(self.project) + '.tex', + self.project, '', 'manual')], + None) + app.add_config_value('latex_logo', None, None, string_classes) + app.add_config_value('latex_appendices', [], None) + app.add_config_value('latex_keep_old_macro_names', True, None) + # now deprecated - use latex_toplevel_sectioning + app.add_config_value('latex_use_parts', False, None) + app.add_config_value('latex_toplevel_sectioning', None, None, [str]) + app.add_config_value('latex_use_modindex', True, None) # deprecated + app.add_config_value('latex_domain_indices', True, None, [list]) + app.add_config_value('latex_show_urls', 'no', None) + app.add_config_value('latex_show_pagerefs', False, None) + # paper_size and font_size are still separate values + # so that you can give them easily on the command line + app.add_config_value('latex_paper_size', 'letter', None) + app.add_config_value('latex_font_size', '10pt', None) + app.add_config_value('latex_elements', {}, None) + app.add_config_value('latex_additional_files', [], None) + + japanese_default = {'manual': 'jsbook', + 'howto': 'jreport'} + app.add_config_value('latex_docclass', + lambda self: japanese_default if self.language == 'ja' else {}, + None) + # now deprecated - use latex_elements + app.add_config_value('latex_preamble', '', None) diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index bb7967e83..f49f4f9a3 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -15,10 +15,9 @@ import codecs import threading from os import path +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,34 +35,7 @@ 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 __init__(self, *args, **kwargs): - Request.__init__(self, *args, **kwargs) - # we do not parse the response in HEAD, so accepting anything is okay - self.headers['Accept-encoding'] = '*' - - def get_method(self): - return 'HEAD' +from sphinx.util.requests import requests, useragent_header, is_ssl_error class AnchorCheckParser(HTMLParser): @@ -79,20 +51,21 @@ class AnchorCheckParser(HTMLParser): for key, value in attrs: if key in ('id', 'name') and value == self.search_anchor: self.found = True + break -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(chunk_size=4096, decode_unicode=True): 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 @@ -101,17 +74,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. @@ -120,9 +82,12 @@ class CheckExternalLinksBuilder(Builder): def init(self): self.to_ignore = [re.compile(x) for x in self.app.config.linkcheck_ignore] + self.anchors_ignore = [re.compile(x) + for x in self.app.config.linkcheck_anchors_ignore] self.good = set() self.broken = {} self.redirected = {} + self.headers = dict(useragent_header) # set a timeout for non-responding servers socket.setdefaulttimeout(5.0) # create output file @@ -143,10 +108,16 @@ 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: req_url, anchor = uri.split('#', 1) + for rex in self.anchors_ignore: + if rex.match(anchor): + anchor = None + break else: req_url = uri anchor = None @@ -158,54 +129,46 @@ class CheckExternalLinksBuilder(Builder): req_url = encode_uri(req_url) try: - if anchor and self.app.config.linkcheck_anchors and \ - not anchor.startswith('!'): + if anchor and self.app.config.linkcheck_anchors: # 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, headers=self.headers, + **kwargs) + found = check_anchor(response, unquote(anchor)) if not found: raise Exception("Anchor '%s' not found" % anchor) else: try: - # try a HEAD request, which should be easier on + # try a HEAD request first, 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, headers=self.headers, **kwargs) + response.raise_for_status() except HTTPError as err: - if err.code not in (403, 405): - raise - # retry with GET if that fails, some servers - # don't like HEAD requests and reply with 403 or 405 - req = Request(req_url) - f = opener.open(req, **kwargs) - f.close() + # retry with GET request if that fails, some servers + # don't like HEAD requests. + response = requests.get(req_url, stream=True, headers=self.headers, + **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 is_ssl_error(err): + return 'ignored', str(err), 0 + else: + return 'broken', str(err), 0 + 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(): @@ -255,7 +218,10 @@ class CheckExternalLinksBuilder(Builder): if lineno: self.info('(line %4d) ' % lineno, nonl=1) if status == 'ignored': - self.info(darkgray('-ignored- ') + uri) + if info: + self.info(darkgray('-ignored- ') + uri + ': ' + info) + else: + self.info(darkgray('-ignored- ') + uri) elif status == 'local': self.info(darkgray('-local- ') + uri) self.write_entry('local', docname, lineno, uri) @@ -264,7 +230,7 @@ class CheckExternalLinksBuilder(Builder): elif status == 'broken': self.write_entry('broken', docname, lineno, uri + ': ' + info) if self.app.quiet or self.app.warningiserror: - self.warn('broken link: %s' % uri, + self.warn('broken link: %s (%s)' % (uri, info), '%s:%s' % (self.env.doc2path(docname), lineno)) else: self.info(red('broken ') + uri + red(' - ' + info)) @@ -321,3 +287,16 @@ class CheckExternalLinksBuilder(Builder): def finish(self): for worker in self.workers: self.wqueue.put((None, None, None), False) + + +def setup(app): + app.add_builder(CheckExternalLinksBuilder) + + app.add_config_value('linkcheck_ignore', [], None) + app.add_config_value('linkcheck_retries', 1, None) + app.add_config_value('linkcheck_timeout', None, None, [int]) + app.add_config_value('linkcheck_workers', 5, None) + app.add_config_value('linkcheck_anchors', True, None) + # Anchors starting with ! are ignored since they are + # commonly used for dynamic pages + app.add_config_value('linkcheck_anchors_ignore', ["^!"], None) diff --git a/sphinx/builders/manpage.py b/sphinx/builders/manpage.py index a2e75f9f7..248ed40b2 100644 --- a/sphinx/builders/manpage.py +++ b/sphinx/builders/manpage.py @@ -19,6 +19,7 @@ from sphinx import addnodes from sphinx.builders import Builder from sphinx.environment import NoUri from sphinx.util.nodes import inline_all_toctrees +from sphinx.util.osutil import make_filename from sphinx.util.console import bold, darkgreen from sphinx.writers.manpage import ManualPageWriter @@ -88,3 +89,13 @@ class ManualPageBuilder(Builder): def finish(self): pass + + +def setup(app): + app.add_builder(ManualPageBuilder) + + app.add_config_value('man_pages', + lambda self: [(self.master_doc, make_filename(self.project).lower(), + '%s %s' % (self.project, self.release), [], 1)], + None) + app.add_config_value('man_show_urls', False, None) diff --git a/sphinx/builders/qthelp.py b/sphinx/builders/qthelp.py index e02985b94..c53b56657 100644 --- a/sphinx/builders/qthelp.py +++ b/sphinx/builders/qthelp.py @@ -21,6 +21,7 @@ from docutils import nodes from sphinx import addnodes from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.util import force_decode +from sphinx.util.osutil import make_filename from sphinx.util.pycompat import htmlescape @@ -104,8 +105,14 @@ class QtHelpBuilder(StandaloneHTMLBuilder): # don't add links add_permalinks = False + # don't add sidebar etc. embedded = True + # disable download role + download_support = False + + # don't generate the search index or include the search page + search = False def init(self): StandaloneHTMLBuilder.init(self) @@ -114,6 +121,9 @@ class QtHelpBuilder(StandaloneHTMLBuilder): self.link_suffix = '.html' # self.config.html_style = 'traditional.css' + def get_theme_config(self): + return self.config.qthelp_theme, self.config.qthelp_theme_options + def handle_finish(self): self.build_qhp(self.outdir, self.config.qthelp_basename) @@ -180,8 +190,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), @@ -192,23 +201,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): @@ -297,3 +301,12 @@ class QtHelpBuilder(StandaloneHTMLBuilder): keywords.extend(self.build_keywords(subitem[0], subitem[1], [])) return keywords + + +def setup(app): + app.setup_extension('sphinx.builders.html') + app.add_builder(QtHelpBuilder) + + app.add_config_value('qthelp_basename', lambda self: make_filename(self.project), None) + app.add_config_value('qthelp_theme', 'nonav', 'html') + app.add_config_value('qthelp_theme_options', {}, 'html') diff --git a/sphinx/builders/texinfo.py b/sphinx/builders/texinfo.py index dec278c86..f070840b6 100644 --- a/sphinx/builders/texinfo.py +++ b/sphinx/builders/texinfo.py @@ -22,7 +22,7 @@ from sphinx.locale import _ from sphinx.builders import Builder from sphinx.environment import NoUri from sphinx.util.nodes import inline_all_toctrees -from sphinx.util.osutil import SEP, copyfile +from sphinx.util.osutil import SEP, copyfile, make_filename from sphinx.util.console import bold, darkgreen from sphinx.writers.texinfo import TexinfoWriter @@ -220,11 +220,25 @@ 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') + + +def setup(app): + app.add_builder(TexinfoBuilder) + + app.add_config_value('texinfo_documents', + lambda self: [(self.master_doc, make_filename(self.project).lower(), + self.project, '', make_filename(self.project), + 'The %s reference manual.' % + make_filename(self.project), + 'Python')], + None) + app.add_config_value('texinfo_appendices', [], None) + app.add_config_value('texinfo_elements', {}, None) + app.add_config_value('texinfo_domain_indices', True, None, [list]) + app.add_config_value('texinfo_show_urls', 'footnote', None) + app.add_config_value('texinfo_no_detailmenu', False, None) diff --git a/sphinx/builders/text.py b/sphinx/builders/text.py index 85da4a1a2..2daf8b043 100644 --- a/sphinx/builders/text.py +++ b/sphinx/builders/text.py @@ -60,13 +60,17 @@ 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)) def finish(self): pass + + +def setup(app): + app.add_builder(TextBuilder) + + app.add_config_value('text_sectionchars', '*=-~"+`', 'env') + app.add_config_value('text_newlines', 'unix', 'env') diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 843b0899e..d8ff5ad8d 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -165,3 +165,7 @@ class WebSupportBuilder(PickleHTMLBuilder): def dump_search_index(self): self.indexer.finish_indexing() + + +def setup(app): + app.add_builder(WebSupportBuilder) diff --git a/sphinx/builders/xml.py b/sphinx/builders/xml.py index 91cb273f5..e0e33312c 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)) @@ -98,3 +95,10 @@ class PseudoXMLBuilder(XMLBuilder): out_suffix = '.pseudoxml' _writer_class = PseudoXMLWriter + + +def setup(app): + app.add_builder(XMLBuilder) + app.add_builder(PseudoXMLBuilder) + + app.add_config_value('xml_pretty', True, 'env') |