summaryrefslogtreecommitdiff
path: root/sphinx/directives/code.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/directives/code.py')
-rw-r--r--sphinx/directives/code.py456
1 files changed, 263 insertions, 193 deletions
diff --git a/sphinx/directives/code.py b/sphinx/directives/code.py
index 3aeb98640..058c95913 100644
--- a/sphinx/directives/code.py
+++ b/sphinx/directives/code.py
@@ -15,13 +15,20 @@ from docutils import nodes
from docutils.parsers.rst import Directive, directives
from docutils.statemachine import ViewList
-from six import string_types
-
from sphinx import addnodes
from sphinx.locale import _
+from sphinx.util import logging
from sphinx.util import parselinenos
from sphinx.util.nodes import set_source_info
+if False:
+ # For type annotation
+ from typing import Any, Dict, List, Tuple # NOQA
+ from sphinx.application import Sphinx # NOQA
+ from sphinx.config import Config # NOQA
+
+logger = logging.getLogger(__name__)
+
class Highlight(Directive):
"""
@@ -38,6 +45,7 @@ class Highlight(Directive):
}
def run(self):
+ # type: () -> List[nodes.Node]
if 'linenothreshold' in self.options:
try:
linenothreshold = int(self.options['linenothreshold'])
@@ -49,10 +57,14 @@ class Highlight(Directive):
linenothreshold=linenothreshold)]
-def dedent_lines(lines, dedent):
+def dedent_lines(lines, dedent, location=None):
+ # type: (List[unicode], int, Any) -> List[unicode]
if not dedent:
return lines
+ if any(s[:dedent].strip() for s in lines):
+ logger.warning(_('Over dedent has detected'), location=location)
+
new_lines = []
for line in lines:
new_line = line[dedent:]
@@ -64,13 +76,15 @@ def dedent_lines(lines, dedent):
def container_wrapper(directive, literal_node, caption):
+ # type: (Directive, nodes.Node, unicode) -> nodes.container
container_node = nodes.container('', literal_block=True,
classes=['literal-block-wrapper'])
parsed = nodes.Element()
directive.state.nested_parse(ViewList([caption], source=''),
directive.content_offset, parsed)
if isinstance(parsed[0], nodes.system_message):
- raise ValueError(parsed[0])
+ msg = _('Invalid caption: %s' % parsed[0].astext())
+ raise ValueError(msg)
caption_node = nodes.caption(parsed[0].rawsource, '',
*parsed[0].children)
caption_node.source = literal_node.source
@@ -101,22 +115,31 @@ class CodeBlock(Directive):
}
def run(self):
+ # type: () -> List[nodes.Node]
+ document = self.state.document
code = u'\n'.join(self.content)
+ location = self.state_machine.get_source_and_line(self.lineno)
linespec = self.options.get('emphasize-lines')
if linespec:
try:
nlines = len(self.content)
- hl_lines = [x + 1 for x in parselinenos(linespec, nlines)]
+ hl_lines = parselinenos(linespec, nlines)
+ if any(i >= nlines for i in hl_lines):
+ logger.warning('line number spec is out of range(1-%d): %r' %
+ (nlines, self.options['emphasize_lines']),
+ location=location)
+
+ hl_lines = [x + 1 for x in hl_lines if x < nlines]
except ValueError as err:
- document = self.state.document
return [document.reporter.warning(str(err), line=self.lineno)]
else:
hl_lines = None
if 'dedent' in self.options:
+ location = self.state_machine.get_source_and_line(self.lineno)
lines = code.split('\n')
- lines = dedent_lines(lines, self.options['dedent'])
+ lines = dedent_lines(lines, self.options['dedent'], location=location)
code = '\n'.join(lines)
literal = nodes.literal_block(code, code)
@@ -136,9 +159,7 @@ class CodeBlock(Directive):
try:
literal = container_wrapper(self, literal, caption)
except ValueError as exc:
- document = self.state.document
- errmsg = _('Invalid caption: %s' % exc[0][0].astext())
- return [document.reporter.warning(errmsg, line=self.lineno)]
+ return [document.reporter.warning(str(exc), line=self.lineno)]
# literal will be note_implicit_target that is linked from caption and numref.
# when options['name'] is provided, it should be primary ID.
@@ -147,6 +168,196 @@ class CodeBlock(Directive):
return [literal]
+class LiteralIncludeReader(object):
+ INVALID_OPTIONS_PAIR = [
+ ('lineno-match', 'lineno-start'),
+ ('lineno-match', 'append'),
+ ('lineno-match', 'prepend'),
+ ('start-after', 'start-at'),
+ ('end-before', 'end-at'),
+ ('diff', 'pyobject'),
+ ('diff', 'lineno-start'),
+ ('diff', 'lineno-match'),
+ ('diff', 'lines'),
+ ('diff', 'start-after'),
+ ('diff', 'end-before'),
+ ('diff', 'start-at'),
+ ('diff', 'end-at'),
+ ]
+
+ def __init__(self, filename, options, config):
+ # type: (unicode, Dict, Config) -> None
+ self.filename = filename
+ self.options = options
+ self.encoding = options.get('encoding', config.source_encoding)
+ self.lineno_start = self.options.get('lineno-start', 1)
+
+ self.parse_options()
+
+ def parse_options(self):
+ # type: () -> None
+ for option1, option2 in self.INVALID_OPTIONS_PAIR:
+ if option1 in self.options and option2 in self.options:
+ raise ValueError(_('Cannot use both "%s" and "%s" options') %
+ (option1, option2))
+
+ def read_file(self, filename, location=None):
+ # type: (unicode, Any) -> List[unicode]
+ try:
+ with codecs.open(filename, 'r', self.encoding, errors='strict') as f: # type: ignore # NOQA
+ text = f.read() # type: unicode
+ if 'tab-width' in self.options:
+ text = text.expandtabs(self.options['tab-width'])
+
+ lines = text.splitlines(True)
+ if 'dedent' in self.options:
+ return dedent_lines(lines, self.options.get('dedent'), location=location)
+ else:
+ return lines
+ except (IOError, OSError):
+ raise IOError(_('Include file %r not found or reading it failed') % filename)
+ except UnicodeError:
+ raise UnicodeError(_('Encoding %r used for reading included file %r seems to '
+ 'be wrong, try giving an :encoding: option') %
+ (self.encoding, filename))
+
+ def read(self, location=None):
+ # type: (Any) -> Tuple[unicode, int]
+ if 'diff' in self.options:
+ lines = self.show_diff()
+ else:
+ filters = [self.pyobject_filter,
+ self.start_filter,
+ self.end_filter,
+ self.lines_filter,
+ self.prepend_filter,
+ self.append_filter]
+ lines = self.read_file(self.filename, location=location)
+ for func in filters:
+ lines = func(lines, location=location)
+
+ return ''.join(lines), len(lines)
+
+ def show_diff(self, location=None):
+ # type: (Any) -> List[unicode]
+ new_lines = self.read_file(self.filename)
+ old_filename = self.options.get('diff')
+ old_lines = self.read_file(old_filename)
+ diff = unified_diff(old_lines, new_lines, old_filename, self.filename) # type: ignore
+ return list(diff)
+
+ def pyobject_filter(self, lines, location=None):
+ # type: (List[unicode], Any) -> List[unicode]
+ pyobject = self.options.get('pyobject')
+ if pyobject:
+ from sphinx.pycode import ModuleAnalyzer
+ analyzer = ModuleAnalyzer.for_file(self.filename, '')
+ tags = analyzer.find_tags()
+ if pyobject not in tags:
+ raise ValueError(_('Object named %r not found in include file %r') %
+ (pyobject, self.filename))
+ else:
+ start = tags[pyobject][1]
+ end = tags[pyobject][2]
+ lines = lines[start - 1:end - 1]
+ if 'lineno-match' in self.options:
+ self.lineno_start = start
+
+ return lines
+
+ def lines_filter(self, lines, location=None):
+ # type: (List[unicode], Any) -> List[unicode]
+ linespec = self.options.get('lines')
+ if linespec:
+ linelist = parselinenos(linespec, len(lines))
+ if any(i >= len(lines) for i in linelist):
+ logger.warning('line number spec is out of range(1-%d): %r' %
+ (len(lines), linespec), location=location)
+
+ if 'lineno-match' in self.options:
+ # make sure the line list is not "disjoint".
+ first = linelist[0]
+ if all(first + i == n for i, n in enumerate(linelist)):
+ self.lineno_start += linelist[0]
+ else:
+ raise ValueError(_('Cannot use "lineno-match" with a disjoint '
+ 'set of "lines"'))
+
+ lines = [lines[n] for n in linelist if n < len(lines)]
+ if lines == []:
+ raise ValueError(_('Line spec %r: no lines pulled from include file %r') %
+ (linespec, self.filename))
+
+ return lines
+
+ def start_filter(self, lines, location=None):
+ # type: (List[unicode], Any) -> List[unicode]
+ if 'start-at' in self.options:
+ start = self.options.get('start-at')
+ inclusive = False
+ elif 'start-after' in self.options:
+ start = self.options.get('start-after')
+ inclusive = True
+ else:
+ start = None
+
+ if start:
+ for lineno, line in enumerate(lines):
+ if start in line:
+ if inclusive:
+ if 'lineno-match' in self.options:
+ self.lineno_start += lineno + 1
+
+ return lines[lineno + 1:]
+ else:
+ if 'lineno-match' in self.options:
+ self.lineno_start += lineno
+
+ return lines[lineno:]
+
+ return lines
+
+ def end_filter(self, lines, location=None):
+ # type: (List[unicode], Any) -> List[unicode]
+ if 'end-at' in self.options:
+ end = self.options.get('end-at')
+ inclusive = True
+ elif 'end-before' in self.options:
+ end = self.options.get('end-before')
+ inclusive = False
+ else:
+ end = None
+
+ if end:
+ for lineno, line in enumerate(lines):
+ if end in line:
+ if inclusive:
+ return lines[:lineno + 1]
+ else:
+ if lineno == 0:
+ return []
+ else:
+ return lines[:lineno]
+
+ return lines
+
+ def prepend_filter(self, lines, location=None):
+ # type: (List[unicode], Any) -> List[unicode]
+ prepend = self.options.get('prepend')
+ if prepend:
+ lines.insert(0, prepend + '\n')
+
+ return lines
+
+ def append_filter(self, lines, location=None):
+ # type: (List[unicode], Any) -> List[unicode]
+ append = self.options.get('append')
+ if append:
+ lines.append(append + '\n')
+
+ return lines
+
+
class LiteralInclude(Directive):
"""
Like ``.. include:: :literal:``, but only warns if the include file is
@@ -181,203 +392,62 @@ class LiteralInclude(Directive):
'diff': directives.unchanged_required,
}
- def read_with_encoding(self, filename, document, codec_info, encoding):
- try:
- 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,
- line=self.lineno)]
- except UnicodeError:
- return [document.reporter.warning(
- 'Encoding %r used for reading included file %r seems to '
- 'be wrong, try giving an :encoding: option' %
- (encoding, filename))]
-
def run(self):
+ # type: () -> List[nodes.Node]
document = self.state.document
if not document.settings.file_insertion_enabled:
return [document.reporter.warning('File insertion disabled',
line=self.lineno)]
env = document.settings.env
- rel_filename, filename = env.relfn2path(self.arguments[0])
-
- if 'pyobject' in self.options and 'lines' in self.options:
- return [document.reporter.warning(
- 'Cannot use both "pyobject" and "lines" options',
- line=self.lineno)]
-
- if 'lineno-match' in self.options and 'lineno-start' in self.options:
- return [document.reporter.warning(
- 'Cannot use both "lineno-match" and "lineno-start"',
- line=self.lineno)]
-
- if 'lineno-match' in self.options and \
- (set(['append', 'prepend']) & set(self.options.keys())):
- return [document.reporter.warning(
- 'Cannot use "lineno-match" and "append" or "prepend"',
- line=self.lineno)]
-
- if 'start-after' in self.options and 'start-at' in self.options:
- return [document.reporter.warning(
- 'Cannot use both "start-after" and "start-at" options',
- line=self.lineno)]
-
- if 'end-before' in self.options and 'end-at' in self.options:
- return [document.reporter.warning(
- 'Cannot use both "end-before" and "end-at" options',
- line=self.lineno)]
-
- encoding = self.options.get('encoding', env.config.source_encoding)
- codec_info = codecs.lookup(encoding)
-
- lines = self.read_with_encoding(filename, document,
- codec_info, encoding)
- if lines and not isinstance(lines[0], string_types):
- return lines
-
- diffsource = self.options.get('diff')
- if diffsource is not None:
- tmp, fulldiffsource = env.relfn2path(diffsource)
-
- difflines = self.read_with_encoding(fulldiffsource, document,
- codec_info, encoding)
- if not isinstance(difflines[0], string_types):
- return difflines
- diff = unified_diff(
- difflines,
- lines,
- diffsource,
- self.arguments[0])
- lines = list(diff)
-
- linenostart = self.options.get('lineno-start', 1)
- objectname = self.options.get('pyobject')
- if objectname is not None:
- from sphinx.pycode import ModuleAnalyzer
- analyzer = ModuleAnalyzer.for_file(filename, '')
- tags = analyzer.find_tags()
- if objectname not in tags:
- return [document.reporter.warning(
- 'Object named %r not found in include file %r' %
- (objectname, filename), line=self.lineno)]
- else:
- lines = lines[tags[objectname][1] - 1: tags[objectname][2] - 1]
- if 'lineno-match' in self.options:
- linenostart = tags[objectname][1]
- linespec = self.options.get('lines')
- if linespec:
- try:
- linelist = parselinenos(linespec, len(lines))
- except ValueError as err:
- return [document.reporter.warning(str(err), line=self.lineno)]
+ # convert options['diff'] to absolute path
+ if 'diff' in self.options:
+ _, path = env.relfn2path(self.options['diff'])
+ self.options['diff'] = path
- if 'lineno-match' in self.options:
- # make sure the line list is not "disjoint".
- previous = linelist[0]
- for line_number in linelist[1:]:
- if line_number == previous + 1:
- previous = line_number
- continue
- return [document.reporter.warning(
- 'Cannot use "lineno-match" with a disjoint set of '
- '"lines"', line=self.lineno)]
- linenostart = linelist[0] + 1
- # just ignore non-existing lines
- lines = [lines[i] for i in linelist if i < len(lines)]
- if not lines:
- return [document.reporter.warning(
- 'Line spec %r: no lines pulled from include file %r' %
- (linespec, filename), line=self.lineno)]
-
- linespec = self.options.get('emphasize-lines')
- if linespec:
- try:
- hl_lines = [x + 1 for x in parselinenos(linespec, len(lines))]
- except ValueError as err:
- return [document.reporter.warning(str(err), line=self.lineno)]
- else:
- hl_lines = None
-
- start_str = self.options.get('start-after')
- start_inclusive = False
- if self.options.get('start-at') is not None:
- start_str = self.options.get('start-at')
- start_inclusive = True
- end_str = self.options.get('end-before')
- end_inclusive = False
- if self.options.get('end-at') is not None:
- end_str = self.options.get('end-at')
- end_inclusive = True
- if start_str is not None or end_str is not None:
- use = not start_str
- res = []
- for line_number, line in enumerate(lines):
- if not use and start_str and start_str in line:
- if 'lineno-match' in self.options:
- linenostart += line_number + 1
- use = True
- if start_inclusive:
- res.append(line)
- elif use and end_str and end_str in line:
- if end_inclusive:
- res.append(line)
- break
- elif use:
- res.append(line)
- lines = res
-
- prepend = self.options.get('prepend')
- if prepend:
- lines.insert(0, prepend + '\n')
-
- append = self.options.get('append')
- if append:
- lines.append(append + '\n')
-
- text = ''.join(lines)
- if self.options.get('tab-width'):
- text = text.expandtabs(self.options['tab-width'])
- retnode = nodes.literal_block(text, text, source=filename)
- set_source_info(self, retnode)
- if diffsource: # if diff is set, set udiff
- retnode['language'] = 'udiff'
- if 'language' in self.options:
- retnode['language'] = self.options['language']
- retnode['linenos'] = 'linenos' in self.options or \
- 'lineno-start' in self.options or \
- 'lineno-match' in self.options
- retnode['classes'] += self.options.get('class', [])
- extra_args = retnode['highlight_args'] = {}
- if hl_lines is not None:
- extra_args['hl_lines'] = hl_lines
- extra_args['linenostart'] = linenostart
- env.note_dependency(rel_filename)
-
- caption = self.options.get('caption')
- if caption is not None:
- if not caption:
- caption = self.arguments[0]
- try:
+ try:
+ location = self.state_machine.get_source_and_line(self.lineno)
+ rel_filename, filename = env.relfn2path(self.arguments[0])
+ env.note_dependency(rel_filename)
+
+ reader = LiteralIncludeReader(filename, self.options, env.config)
+ text, lines = reader.read(location=location)
+
+ retnode = nodes.literal_block(text, text, source=filename)
+ set_source_info(self, retnode)
+ if self.options.get('diff'): # if diff is set, set udiff
+ retnode['language'] = 'udiff'
+ elif 'language' in self.options:
+ retnode['language'] = self.options['language']
+ retnode['linenos'] = ('linenos' in self.options or
+ 'lineno-start' in self.options or
+ 'lineno-match' in self.options)
+ retnode['classes'] += self.options.get('class', [])
+ extra_args = retnode['highlight_args'] = {}
+ if 'empahsize-lines' in self.options:
+ hl_lines = parselinenos(self.options['emphasize-lines'], lines)
+ if any(i >= lines for i in hl_lines):
+ logger.warning('line number spec is out of range(1-%d): %r' %
+ (lines, self.options['emphasize_lines']),
+ location=location)
+ extra_args['hl_lines'] = [x + 1 for x in hl_lines if x < lines]
+ extra_args['linenostart'] = reader.lineno_start
+
+ if 'caption' in self.options:
+ caption = self.options['caption'] or self.arguments[0]
retnode = container_wrapper(self, retnode, caption)
- except ValueError as exc:
- document = self.state.document
- errmsg = _('Invalid caption: %s' % exc[0][0].astext())
- return [document.reporter.warning(errmsg, line=self.lineno)]
- # retnode will be note_implicit_target that is linked from caption and numref.
- # when options['name'] is provided, it should be primary ID.
- self.add_name(retnode)
+ # retnode will be note_implicit_target that is linked from caption and numref.
+ # when options['name'] is provided, it should be primary ID.
+ self.add_name(retnode)
- return [retnode]
+ return [retnode]
+ except Exception as exc:
+ return [document.reporter.warning(str(exc), line=self.lineno)]
def setup(app):
+ # type: (Sphinx) -> Dict[unicode, Any]
directives.register_directive('highlight', Highlight)
directives.register_directive('highlightlang', Highlight) # old
directives.register_directive('code-block', CodeBlock)