EVOLUTION-MANAGER
Edit File: code.py
# -*- coding: utf-8 -*- """ sphinx.directives.code ~~~~~~~~~~~~~~~~~~~~~~ :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import codecs import sys import warnings from difflib import unified_diff from docutils import nodes from docutils.parsers.rst import directives from docutils.statemachine import ViewList from six import text_type from sphinx import addnodes from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.locale import __ from sphinx.util import logging from sphinx.util import parselinenos from sphinx.util.docutils import SphinxDirective 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(SphinxDirective): """ Directive to set the highlighting language for code blocks, as well as the threshold for line numbers. """ has_content = False required_arguments = 1 optional_arguments = 0 final_argument_whitespace = False option_spec = { 'linenothreshold': directives.positive_int, } def run(self): # type: () -> List[nodes.Node] linenothreshold = self.options.get('linenothreshold', sys.maxsize) return [addnodes.highlightlang(lang=self.arguments[0].strip(), linenothreshold=linenothreshold)] class HighlightLang(Highlight): """highlightlang directive (deprecated)""" def run(self): # type: () -> List[nodes.Node] warnings.warn('highlightlang directive is deprecated. ' 'Please use highlight directive instead.', RemovedInSphinx40Warning, stacklevel=2) return Highlight.run(self) 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:] if line.endswith('\n') and not new_line: new_line = '\n' # keep CRLF new_lines.append(new_line) return new_lines def container_wrapper(directive, literal_node, caption): # type: (SphinxDirective, 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): 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 caption_node.line = literal_node.line container_node += caption_node container_node += literal_node return container_node class CodeBlock(SphinxDirective): """ Directive for a code block with special highlighting or line numbering settings. """ has_content = True required_arguments = 1 optional_arguments = 0 final_argument_whitespace = False option_spec = { 'linenos': directives.flag, 'dedent': int, 'lineno-start': int, 'emphasize-lines': directives.unchanged_required, 'caption': directives.unchanged_required, 'class': directives.class_option, 'name': directives.unchanged, } 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 = 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: 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'], location=location) code = '\n'.join(lines) literal = nodes.literal_block(code, code) literal['language'] = self.arguments[0] literal['linenos'] = 'linenos' in self.options or \ 'lineno-start' in self.options literal['classes'] += self.options.get('class', []) extra_args = literal['highlight_args'] = {} if hl_lines is not None: extra_args['hl_lines'] = hl_lines if 'lineno-start' in self.options: extra_args['linenostart'] = self.options['lineno-start'] set_source_info(self, literal) caption = self.options.get('caption') if caption: try: literal = container_wrapper(self, literal, caption) except ValueError as exc: return [document.reporter.warning(text_type(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. self.add_name(literal) 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']) return text.splitlines(True) 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, self.dedent_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) 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] 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:] if inclusive is True: raise ValueError('start-after pattern not found: %s' % start) else: raise ValueError('start-at pattern not found: %s' % start) 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] if inclusive is True: raise ValueError('end-at pattern not found: %s' % end) else: raise ValueError('end-before pattern not found: %s' % end) 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 def dedent_filter(self, lines, location=None): # type: (List[unicode], Any) -> List[unicode] if 'dedent' in self.options: return dedent_lines(lines, self.options.get('dedent'), location=location) else: return lines class LiteralInclude(SphinxDirective): """ Like ``.. include:: :literal:``, but only warns if the include file is not found, and does not raise errors. Also has several options for selecting what to include. """ has_content = False required_arguments = 1 optional_arguments = 0 final_argument_whitespace = True option_spec = { 'dedent': int, 'linenos': directives.flag, 'lineno-start': int, 'lineno-match': directives.flag, 'tab-width': int, 'language': directives.unchanged_required, 'encoding': directives.encoding, 'pyobject': directives.unchanged_required, 'lines': directives.unchanged_required, 'start-after': directives.unchanged_required, 'end-before': directives.unchanged_required, 'start-at': directives.unchanged_required, 'end-at': directives.unchanged_required, 'prepend': directives.unchanged_required, 'append': directives.unchanged_required, 'emphasize-lines': directives.unchanged_required, 'caption': directives.unchanged, 'class': directives.class_option, 'name': directives.unchanged, 'diff': directives.unchanged_required, } 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)] # convert options['diff'] to absolute path if 'diff' in self.options: _, path = self.env.relfn2path(self.options['diff']) self.options['diff'] = path try: location = self.state_machine.get_source_and_line(self.lineno) rel_filename, filename = self.env.relfn2path(self.arguments[0]) self.env.note_dependency(rel_filename) reader = LiteralIncludeReader(filename, self.options, self.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 'emphasize-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) # 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] except Exception as exc: return [document.reporter.warning(text_type(exc), line=self.lineno)] def setup(app): # type: (Sphinx) -> Dict[unicode, Any] directives.register_directive('highlight', Highlight) directives.register_directive('highlightlang', HighlightLang) directives.register_directive('code-block', CodeBlock) directives.register_directive('sourcecode', CodeBlock) directives.register_directive('literalinclude', LiteralInclude) return { 'version': 'builtin', 'parallel_read_safe': True, 'parallel_write_safe': True, }