summaryrefslogtreecommitdiff
path: root/sphinx/util
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/util')
-rw-r--r--sphinx/util/__init__.py55
-rw-r--r--sphinx/util/compat.py2
-rw-r--r--sphinx/util/docfields.py28
-rw-r--r--sphinx/util/docutils.py99
-rw-r--r--sphinx/util/fileutil.py82
-rw-r--r--sphinx/util/i18n.py12
-rw-r--r--sphinx/util/inspect.py14
-rw-r--r--sphinx/util/matching.py21
-rw-r--r--sphinx/util/nodes.py49
-rw-r--r--sphinx/util/osutil.py90
-rw-r--r--sphinx/util/png.py15
-rw-r--r--sphinx/util/pycompat.py7
-rw-r--r--sphinx/util/requests.py55
-rw-r--r--sphinx/util/template.py61
14 files changed, 504 insertions, 86 deletions
diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py
index 9c5365aa7..7ac5c62f7 100644
--- a/sphinx/util/__init__.py
+++ b/sphinx/util/__init__.py
@@ -8,6 +8,7 @@
:copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
+from __future__ import absolute_import
import os
import re
@@ -18,20 +19,17 @@ import posixpath
import traceback
import unicodedata
from os import path
-from codecs import open, BOM_UTF8
+from codecs import BOM_UTF8
from collections import deque
from six import iteritems, text_type, binary_type
from six.moves import range
from six.moves.urllib.parse import urlsplit, urlunsplit, quote_plus, parse_qsl, urlencode
-import docutils
from docutils.utils import relative_path
-import jinja2
-
-import sphinx
from sphinx.errors import PycodeError, SphinxParallelError, ExtensionError
from sphinx.util.console import strip_colors
+from sphinx.util.fileutil import copy_asset_file
from sphinx.util.osutil import fs_encoding
# import other utilities; partly for backwards compatibility, so don't
@@ -148,7 +146,7 @@ class FilenameUniqDict(dict):
def copy_static_entry(source, targetdir, builder, context={},
exclude_matchers=(), level=0):
- """Copy a HTML builder static_path entry from source to targetdir.
+ """[DEPRECATED] Copy a HTML builder static_path entry from source to targetdir.
Handles all possible cases of files, directories and subdirectories.
"""
@@ -158,16 +156,7 @@ def copy_static_entry(source, targetdir, builder, context={},
if matcher(relpath):
return
if path.isfile(source):
- target = path.join(targetdir, path.basename(source))
- if source.lower().endswith('_t') and builder.templates:
- # templated!
- fsrc = open(source, 'r', encoding='utf-8')
- fdst = open(target[:-2], 'w', encoding='utf-8')
- fdst.write(builder.templates.render_string(fsrc.read(), context))
- fsrc.close()
- fdst.close()
- else:
- copyfile(source, target)
+ copy_asset_file(source, targetdir, context, builder.templates)
elif path.isdir(source):
if not path.isdir(targetdir):
os.mkdir(targetdir)
@@ -182,37 +171,6 @@ def copy_static_entry(source, targetdir, builder, context={},
exclude_matchers=exclude_matchers)
-def copy_extra_entry(source, targetdir, exclude_matchers=()):
- """Copy a HTML builder extra_path entry from source to targetdir.
-
- Handles all possible cases of files, directories and subdirectories.
- """
- def excluded(path):
- relpath = relative_path(os.path.dirname(source), path)
- return any(matcher(relpath) for matcher in exclude_matchers)
-
- def copy_extra_file(source_, targetdir_):
- if not excluded(source_):
- target = path.join(targetdir_, os.path.basename(source_))
- copyfile(source_, target)
-
- if os.path.isfile(source):
- copy_extra_file(source, targetdir)
- return
-
- for root, dirs, files in os.walk(source):
- reltargetdir = os.path.join(targetdir, relative_path(source, root))
- for dir in dirs[:]:
- if excluded(os.path.join(root, dir)):
- dirs.remove(dir)
- else:
- target = os.path.join(reltargetdir, dir)
- if not path.exists(target):
- os.mkdir(target)
- for file in files:
- copy_extra_file(os.path.join(root, file), reltargetdir)
-
-
_DEBUG_HEADER = '''\
# Sphinx version: %s
# Python version: %s (%s)
@@ -226,6 +184,9 @@ _DEBUG_HEADER = '''\
def save_traceback(app):
"""Save the current exception's traceback in a temporary file."""
+ import sphinx
+ import jinja2
+ import docutils
import platform
exc = sys.exc_info()[1]
if isinstance(exc, SphinxParallelError):
diff --git a/sphinx/util/compat.py b/sphinx/util/compat.py
index 5329cb668..0af65cbe3 100644
--- a/sphinx/util/compat.py
+++ b/sphinx/util/compat.py
@@ -8,6 +8,8 @@
:copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
+from __future__ import absolute_import
+
import warnings
from docutils import nodes
diff --git a/sphinx/util/docfields.py b/sphinx/util/docfields.py
index f4eb703ce..d5cb4038f 100644
--- a/sphinx/util/docfields.py
+++ b/sphinx/util/docfields.py
@@ -9,6 +9,7 @@
:copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
+from __future__ import absolute_import
from docutils import nodes
@@ -62,6 +63,10 @@ class Field(object):
refnode += contnode or innernode(target, target)
return refnode
+ def make_xrefs(self, rolename, domain, target,
+ innernode=addnodes.literal_emphasis, contnode=None):
+ return [self.make_xref(rolename, domain, target, innernode, contnode)]
+
def make_entry(self, fieldarg, content):
return (fieldarg, content)
@@ -70,14 +75,15 @@ class Field(object):
fieldname = nodes.field_name('', self.label)
if fieldarg:
fieldname += nodes.Text(' ')
- fieldname += self.make_xref(self.rolename, domain,
- fieldarg, nodes.Text)
+ fieldname.extend(self.make_xrefs(self.rolename, domain,
+ fieldarg, nodes.Text))
+
if len(content) == 1 and (
isinstance(content[0], nodes.Text) or
(isinstance(content[0], nodes.inline) and len(content[0]) == 1 and
isinstance(content[0][0], nodes.Text))):
- content = [self.make_xref(self.bodyrolename, domain,
- content[0].astext(), contnode=content[0])]
+ content = self.make_xrefs(self.bodyrolename, domain,
+ content[0].astext(), contnode=content[0])
fieldbody = nodes.field_body('', nodes.paragraph('', '', *content))
return nodes.field('', fieldname, fieldbody)
@@ -108,14 +114,16 @@ class GroupedField(Field):
listnode = self.list_type()
for fieldarg, content in items:
par = nodes.paragraph()
- par += self.make_xref(self.rolename, domain, fieldarg,
- addnodes.literal_strong)
+ par.extend(self.make_xrefs(self.rolename, domain, fieldarg,
+ addnodes.literal_strong))
par += nodes.Text(' -- ')
par += content
listnode += nodes.list_item('', par)
+
if len(items) == 1 and self.can_collapse:
fieldbody = nodes.field_body('', listnode[0][0])
return nodes.field('', fieldname, fieldbody)
+
fieldbody = nodes.field_body('', listnode)
return nodes.field('', fieldname, fieldbody)
@@ -150,8 +158,8 @@ class TypedField(GroupedField):
def make_field(self, types, domain, items):
def handle_item(fieldarg, content):
par = nodes.paragraph()
- par += self.make_xref(self.rolename, domain, fieldarg,
- addnodes.literal_strong)
+ par.extend(self.make_xrefs(self.rolename, domain, fieldarg,
+ addnodes.literal_strong))
if fieldarg in types:
par += nodes.Text(' (')
# NOTE: using .pop() here to prevent a single type node to be
@@ -160,8 +168,8 @@ class TypedField(GroupedField):
fieldtype = types.pop(fieldarg)
if len(fieldtype) == 1 and isinstance(fieldtype[0], nodes.Text):
typename = u''.join(n.astext() for n in fieldtype)
- par += self.make_xref(self.typerolename, domain, typename,
- addnodes.literal_emphasis)
+ par.extend(self.make_xrefs(self.typerolename, domain, typename,
+ addnodes.literal_emphasis))
else:
par += fieldtype
par += nodes.Text(')')
diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py
new file mode 100644
index 000000000..be9e2edad
--- /dev/null
+++ b/sphinx/util/docutils.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.util.docutils
+ ~~~~~~~~~~~~~~~~~~~~
+
+ Utility functions for docutils.
+
+ :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+from __future__ import absolute_import
+
+from copy import copy
+from contextlib import contextmanager
+from docutils.parsers.rst import directives, roles
+
+
+@contextmanager
+def docutils_namespace():
+ """Create namespace for reST parsers."""
+ try:
+ _directives = copy(directives._directives)
+ _roles = copy(roles._roles)
+
+ yield
+ finally:
+ directives._directives = _directives
+ roles._roles = _roles
+
+
+class ElementLookupError(Exception):
+ pass
+
+
+class sphinx_domains(object):
+ """Monkey-patch directive and role dispatch, so that domain-specific
+ markup takes precedence.
+ """
+ def __init__(self, env):
+ self.env = env
+ self.directive_func = None
+ self.roles_func = None
+
+ def __enter__(self):
+ self.enable()
+
+ def __exit__(self, type, value, traceback):
+ self.disable()
+
+ def enable(self):
+ self.directive_func = directives.directive
+ self.role_func = roles.role
+
+ directives.directive = self.lookup_directive
+ roles.role = self.lookup_role
+
+ def disable(self):
+ directives.directive = self.directive_func
+ roles.role = self.role_func
+
+ def lookup_domain_element(self, type, name):
+ """Lookup a markup element (directive or role), given its name which can
+ be a full name (with domain).
+ """
+ name = name.lower()
+ # explicit domain given?
+ if ':' in name:
+ domain_name, name = name.split(':', 1)
+ if domain_name in self.env.domains:
+ domain = self.env.domains[domain_name]
+ element = getattr(domain, type)(name)
+ if element is not None:
+ return element, []
+ # else look in the default domain
+ else:
+ def_domain = self.env.temp_data.get('default_domain')
+ if def_domain is not None:
+ element = getattr(def_domain, type)(name)
+ if element is not None:
+ return element, []
+
+ # always look in the std domain
+ element = getattr(self.env.domains['std'], type)(name)
+ if element is not None:
+ return element, []
+
+ raise ElementLookupError
+
+ def lookup_directive(self, name, lang_module, document):
+ try:
+ return self.lookup_domain_element('directive', name)
+ except ElementLookupError:
+ return self.directive_func(name, lang_module, document)
+
+ def lookup_role(self, name, lang_module, lineno, reporter):
+ try:
+ return self.lookup_domain_element('role', name)
+ except ElementLookupError:
+ return self.role_func(name, lang_module, lineno, reporter)
diff --git a/sphinx/util/fileutil.py b/sphinx/util/fileutil.py
new file mode 100644
index 000000000..4375b7e61
--- /dev/null
+++ b/sphinx/util/fileutil.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.util.fileutil
+ ~~~~~~~~~~~~~~~~~~~~
+
+ File utility functions for Sphinx.
+
+ :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+from __future__ import absolute_import
+
+import os
+import codecs
+import posixpath
+from docutils.utils import relative_path
+from sphinx.util.osutil import copyfile, ensuredir, walk
+
+
+def copy_asset_file(source, destination, context=None, renderer=None):
+ """Copy an asset file to destination.
+
+ On copying, it expands the template variables if context argument is given and
+ the asset is a template file.
+
+ :param source: The path to source file
+ :param destination: The path to destination file or directory
+ :param context: The template variables. If not given, template files are simply copied
+ :param renderer: The template engine. If not given, SphinxRenderer is used by default
+ """
+ if not os.path.exists(source):
+ return
+
+ if os.path.exists(destination) and os.path.isdir(destination):
+ # Use source filename if destination points a directory
+ destination = os.path.join(destination, os.path.basename(source))
+
+ if source.lower().endswith('_t') and context:
+ if renderer is None:
+ from sphinx.util.template import SphinxRenderer
+ renderer = SphinxRenderer()
+
+ with codecs.open(source, 'r', encoding='utf-8') as fsrc:
+ with codecs.open(destination[:-2], 'w', encoding='utf-8') as fdst:
+ fdst.write(renderer.render_string(fsrc.read(), context))
+ else:
+ copyfile(source, destination)
+
+
+def copy_asset(source, destination, excluded=lambda path: False, context=None, renderer=None):
+ """Copy asset files to destination recursively.
+
+ On copying, it expands the template variables if context argument is given and
+ the asset is a template file.
+
+ :param source: The path to source file or directory
+ :param destination: The path to destination directory
+ :param excluded: The matcher to determine the given path should be copied or not
+ :param context: The template variables. If not given, template files are simply copied
+ :param renderer: The template engine. If not given, SphinxRenderer is used by default
+ """
+ if not os.path.exists(source):
+ return
+
+ ensuredir(destination)
+ if os.path.isfile(source):
+ copy_asset_file(source, destination, context, renderer)
+ return
+
+ for root, dirs, files in walk(source):
+ reldir = relative_path(source, root)
+ for dir in dirs[:]:
+ if excluded(posixpath.join(reldir, dir)):
+ dirs.remove(dir)
+ else:
+ ensuredir(posixpath.join(destination, reldir, dir))
+
+ for filename in files:
+ if not excluded(posixpath.join(reldir, filename)):
+ copy_asset_file(posixpath.join(root, filename),
+ posixpath.join(destination, reldir),
+ context, renderer)
diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py
index 5b396820c..112353d47 100644
--- a/sphinx/util/i18n.py
+++ b/sphinx/util/i18n.py
@@ -230,10 +230,16 @@ def get_image_filename_for_language(filename, env):
return filename
filename_format = env.config.figure_language_filename
- root, ext = path.splitext(filename)
+ d = dict()
+ d['root'], d['ext'] = path.splitext(filename)
+ dirname = path.dirname(d['root'])
+ if dirname and not dirname.endswith(path.sep):
+ dirname += path.sep
+ d['path'] = dirname
+ d['basename'] = path.basename(d['root'])
+ d['language'] = env.config.language
try:
- return filename_format.format(root=root, ext=ext,
- language=env.config.language)
+ return filename_format.format(**d)
except KeyError as exc:
raise SphinxError('Invalid figure_language_filename: %r' % exc)
diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py
index 81ffe25c8..147d43592 100644
--- a/sphinx/util/inspect.py
+++ b/sphinx/util/inspect.py
@@ -60,7 +60,7 @@ if PY3:
raise TypeError('%r is not a Python function' % func)
return inspect.getfullargspec(func)
-else: # 2.6, 2.7
+else: # 2.7
from functools import partial
def getargspec(func):
@@ -94,6 +94,18 @@ else: # 2.6, 2.7
pass
return inspect.ArgSpec(args, varargs, varkw, func_defaults)
+try:
+ import enum
+except ImportError:
+ enum = None
+
+
+def isenumattribute(x):
+ """Check if the object is attribute of enum."""
+ if enum is None:
+ return False
+ return isinstance(x, enum.Enum)
+
def isdescriptor(x):
"""Check if the object is some kind of descriptor."""
diff --git a/sphinx/util/matching.py b/sphinx/util/matching.py
index 91fda6378..fc7750be9 100644
--- a/sphinx/util/matching.py
+++ b/sphinx/util/matching.py
@@ -62,6 +62,27 @@ def compile_matchers(patterns):
return [re.compile(_translate_pattern(pat)).match for pat in patterns]
+class Matcher(object):
+ """A pattern matcher for Multiple shell-style glob patterns.
+
+ Note: this modifies the patterns to work with copy_asset().
+ For example, "**/index.rst" matches with "index.rst"
+ """
+
+ def __init__(self, patterns):
+ expanded = [pat[3:] for pat in patterns if pat.startswith('**/')]
+ self.patterns = compile_matchers(patterns + expanded)
+
+ def __call__(self, string):
+ return self.match(string)
+
+ def match(self, string):
+ return any(pat(string) for pat in self.patterns)
+
+
+DOTFILES = Matcher(['**/.*'])
+
+
_pat_cache = {}
diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py
index e4a2fd73b..fe3b0f2f9 100644
--- a/sphinx/util/nodes.py
+++ b/sphinx/util/nodes.py
@@ -8,6 +8,7 @@
:copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
+from __future__ import absolute_import
import re
@@ -85,7 +86,18 @@ IGNORED_NODES = (
)
+def is_pending_meta(node):
+ if (isinstance(node, nodes.pending) and
+ isinstance(node.details.get('nodes', [None])[0], addnodes.meta)):
+ return True
+ else:
+ return False
+
+
def is_translatable(node):
+ if isinstance(node, addnodes.translatable):
+ return True
+
if isinstance(node, nodes.TextElement):
if not node.source:
return False # built-in message
@@ -103,6 +115,11 @@ def is_translatable(node):
if isinstance(node, nodes.image) and node.get('translatable'):
return True
+ if isinstance(node, addnodes.meta):
+ return True
+ if is_pending_meta(node):
+ return True
+
return False
@@ -114,11 +131,18 @@ LITERAL_TYPE_NODES = (
IMAGE_TYPE_NODES = (
nodes.image,
)
+META_TYPE_NODES = (
+ addnodes.meta,
+)
def extract_messages(doctree):
"""Extract translatable messages from a document tree."""
for node in doctree.traverse(is_translatable):
+ if isinstance(node, addnodes.translatable):
+ for msg in node.extract_original_messages():
+ yield node, msg
+ continue
if isinstance(node, LITERAL_TYPE_NODES):
msg = node.rawsource
if not msg:
@@ -127,6 +151,10 @@ def extract_messages(doctree):
msg = '.. image:: %s' % node['uri']
if node.get('alt'):
msg += '\n :alt: %s' % node['alt']
+ elif isinstance(node, META_TYPE_NODES):
+ msg = node.rawcontent
+ elif is_pending_meta(node):
+ msg = node.details['nodes'][0].rawcontent
else:
msg = node.rawsource.replace('\n', ' ').strip()
@@ -293,6 +321,27 @@ def set_role_source_info(inliner, lineno, node):
node.source, node.line = inliner.reporter.get_source_and_line(lineno)
+def process_only_nodes(doctree, tags, warn_node=None):
+ # A comment on the comment() nodes being inserted: replacing by [] would
+ # result in a "Losing ids" exception if there is a target node before
+ # the only node, so we make sure docutils can transfer the id to
+ # something, even if it's just a comment and will lose the id anyway...
+ for node in doctree.traverse(addnodes.only):
+ try:
+ ret = tags.eval_condition(node['expr'])
+ except Exception as err:
+ if warn_node is None:
+ raise err
+ warn_node('exception while evaluating only '
+ 'directive expression: %s' % err, node)
+ node.replace_self(node.children or nodes.comment())
+ else:
+ if ret:
+ node.replace_self(node.children or nodes.comment())
+ else:
+ node.replace_self(nodes.comment())
+
+
# monkey-patch Element.copy to copy the rawsource and line
def _new_copy(self):
diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py
index b416a8c1f..b8fffb220 100644
--- a/sphinx/util/osutil.py
+++ b/sphinx/util/osutil.py
@@ -17,8 +17,10 @@ import time
import errno
import locale
import shutil
+import filecmp
from os import path
import contextlib
+from io import BytesIO, StringIO
from six import PY2, text_type
@@ -78,8 +80,8 @@ def ensuredir(path):
raise
-# This function is same as os.walk of Python2.6, 2.7, 3.2, 3.3 except a
-# customization that check UnicodeError.
+# This function is same as os.walk of Python2.7 except a customization
+# that check UnicodeError.
# The customization obstacle to replace the function with the os.walk.
def walk(top, topdown=True, followlinks=False):
"""Backport of os.walk from 2.6, where the *followlinks* argument was
@@ -141,13 +143,16 @@ def copytimes(source, dest):
def copyfile(source, dest):
- """Copy a file and its modification times, if possible."""
- shutil.copyfile(source, dest)
- try:
- # don't do full copystat because the source may be read-only
- copytimes(source, dest)
- except OSError:
- pass
+ """Copy a file and its modification times, if possible.
+
+ Note: ``copyfile`` skips copying if the file has not been changed"""
+ if not path.exists(dest) or not filecmp.cmp(source, dest):
+ shutil.copyfile(source, dest)
+ try:
+ # don't do full copystat because the source may be read-only
+ copytimes(source, dest)
+ except OSError:
+ pass
no_fn_re = re.compile(r'[^a-zA-Z0-9_-]')
@@ -215,6 +220,73 @@ def cd(target_dir):
os.chdir(cwd)
+class FileAvoidWrite(object):
+ """File-like object that buffers output and only writes if content changed.
+
+ Use this class like when writing to a file to avoid touching the original
+ file if the content hasn't changed. This is useful in scenarios where file
+ mtime is used to invalidate caches or trigger new behavior.
+
+ When writing to this file handle, all writes are buffered until the object
+ is closed.
+
+ Objects can be used as context managers.
+ """
+ def __init__(self, path):
+ self._path = path
+ self._io = None
+
+ def write(self, data):
+ if not self._io:
+ if isinstance(data, text_type):
+ self._io = StringIO()
+ else:
+ self._io = BytesIO()
+
+ self._io.write(data)
+
+ def close(self):
+ """Stop accepting writes and write file, if needed."""
+ if not self._io:
+ raise Exception('FileAvoidWrite does not support empty files.')
+
+ buf = self.getvalue()
+ self._io.close()
+
+ r_mode = 'r'
+ w_mode = 'w'
+ if isinstance(self._io, BytesIO):
+ r_mode = 'rb'
+ w_mode = 'wb'
+
+ old_content = None
+
+ try:
+ with open(self._path, r_mode) as old_f:
+ old_content = old_f.read()
+ if old_content == buf:
+ return
+ except IOError:
+ pass
+
+ with open(self._path, w_mode) as f:
+ f.write(buf)
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self.close()
+
+ def __getattr__(self, name):
+ # Proxy to _io instance.
+ if not self._io:
+ raise Exception('Must write to FileAvoidWrite before other '
+ 'methods can be used')
+
+ return getattr(self._io, name)
+
+
def rmtree(path):
if os.path.isdir(path):
shutil.rmtree(path)
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 4503c34e1..e3b17ef62 100644
--- a/sphinx/util/pycompat.py
+++ b/sphinx/util/pycompat.py
@@ -55,7 +55,7 @@ if PY3:
return text_type(tree)
from html import escape as htmlescape # noqa: >= Python 3.2
- class UnicodeMixin:
+ class UnicodeMixin(object):
"""Mixin class to handle defining the proper __str__/__unicode__
methods in Python 2 or 3."""
@@ -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/util/requests.py b/sphinx/util/requests.py
new file mode 100644
index 000000000..36ac1e0e7
--- /dev/null
+++ b/sphinx/util/requests.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.util.requests
+ ~~~~~~~~~~~~~~~~~~~~
+
+ Simple requests package loader
+
+ :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+from __future__ import absolute_import
+
+import requests
+import warnings
+import pkg_resources
+from requests.packages.urllib3.exceptions import SSLError
+
+# try to load requests[security]
+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+).'
+ )
+except pkg_resources.UnknownExtra:
+ warnings.warn(
+ 'Some links may return broken results due to being unable to '
+ 'check the Server Name Indication (SNI) in the returned SSL cert '
+ 'against the hostname in the url requested. Recommended to '
+ 'install requests-2.4.1+.'
+ )
+
+useragent_header = [('User-Agent',
+ 'Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0')]
+
+
+def is_ssl_error(exc):
+ if isinstance(exc, SSLError):
+ return True
+ else:
+ args = getattr(exc, 'args', [])
+ if args and isinstance(args[0], SSLError):
+ return True
+ else:
+ return False
diff --git a/sphinx/util/template.py b/sphinx/util/template.py
new file mode 100644
index 000000000..7cb897e7d
--- /dev/null
+++ b/sphinx/util/template.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.util.template
+ ~~~~~~~~~~~~~~~~~~~~
+
+ Templates utility functions for Sphinx.
+
+ :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import os
+from jinja2.sandbox import SandboxedEnvironment
+
+from sphinx import package_dir
+from sphinx.jinja2glue import SphinxFileSystemLoader
+
+
+class BaseRenderer(object):
+ def __init__(self, loader=None):
+ self.env = SandboxedEnvironment(loader=loader)
+ self.env.filters['repr'] = repr
+
+ def render(self, template_name, context):
+ return self.env.get_template(template_name).render(context)
+
+ def render_string(self, source, context):
+ return self.env.from_string(source).render(context)
+
+
+class FileRenderer(BaseRenderer):
+ def __init__(self, search_path):
+ loader = SphinxFileSystemLoader(search_path)
+ super(FileRenderer, self).__init__(loader)
+
+ @classmethod
+ def render_from_file(cls, filename, context):
+ dirname = os.path.dirname(filename)
+ basename = os.path.basename(filename)
+ return cls(dirname).render(basename, context)
+
+
+class SphinxRenderer(FileRenderer):
+ def __init__(self):
+ super(SphinxRenderer, self).__init__(os.path.join(package_dir, 'templates'))
+
+ @classmethod
+ def render_from_file(cls, filename, context):
+ return FileRenderer.render_from_file(filename, context)
+
+
+class LaTeXRenderer(SphinxRenderer):
+ def __init__(self):
+ super(LaTeXRenderer, self).__init__()
+
+ # use JSP/eRuby like tagging instead because curly bracket; the default
+ # tagging of jinja2 is not good for LaTeX sources.
+ self.env.variable_start_string = '<%='
+ self.env.variable_end_string = '%>'
+ self.env.block_start_string = '<%'
+ self.env.block_end_string = '%>'