pax_global_header00006660000000000000000000000064133713146650014523gustar00rootroot0000000000000052 comment=8773401c1a1c1ae1eb9c8834efffd7cf612bacb1 rstcheck-3.3.1/000077500000000000000000000000001337131466500133355ustar00rootroot00000000000000rstcheck-3.3.1/.gitignore000066400000000000000000000001211337131466500153170ustar00rootroot00000000000000.*.swp .eggs/ dist build *.pyc *~ *.egg-info *.aux *.toc *.log *.nav *.out *.snm rstcheck-3.3.1/.travis.yml000066400000000000000000000010731337131466500154470ustar00rootroot00000000000000sudo: false language: python python: - "2.7" - "3.4" - "3.5" - "3.6" - "nightly" - "pypy-5.4" env: - - SPHINX=sphinx>1.7,<=1.8 - SPHINX=sphinx>1.6,<=1.7 - SPHINX=sphinx>1.5,<=1.6 install: - pip install --retries=20 . - pip install --retries=20 pycodestyle pyflakes - if [[ -n "$SPHINX" ]]; then pip install --retries=20 "$SPHINX"; fi script: - if [[ -n "$SPHINX" ]]; then ./rstcheck.py -h | grep 'Sphinx is enabled'; fi - ./test_rstcheck.py - ./test.bash - pycodestyle ./*.py - pyflakes ./*.py rstcheck-3.3.1/AUTHORS.rst000066400000000000000000000005761337131466500152240ustar00rootroot00000000000000Author ------ - Steven Myint (https://github.com/myint) Patches ------- - 王昌旭 (https://github.com/DelightRun) - LCD 047 (https://github.com/lcd047) - uralbash (https://github.com/uralbash) - Michael Fladischer (https://github.com/fladi) - biscuitsnake (https://github.com/biscuitsnake) - Maël Pedretti (https://github.com/73VW) - Santos Gallegos (https://github.com/stsewd) rstcheck-3.3.1/LICENSE000066400000000000000000000020451337131466500143430ustar00rootroot00000000000000Copyright (C) 2013-2017 Steven Myint Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. rstcheck-3.3.1/MANIFEST.in000066400000000000000000000002271337131466500150740ustar00rootroot00000000000000include AUTHORS.rst include README.rst include LICENSE exclude .travis.yml exclude Makefile exclude test.bash exclude test_rstcheck.py prune examples rstcheck-3.3.1/Makefile000066400000000000000000000011251337131466500147740ustar00rootroot00000000000000default: check test # These checkers are more obscure. So consider them optional. Only run them if # they are installed. CHECK_MANIFEST := $(shell command -v check-manifest 2> /dev/null) SCSPELL := $(shell command -v scspell 2> /dev/null) check: pycodestyle rstcheck.py setup.py python setup.py --long-description | ./rstcheck.py - ./rstcheck.py README.rst ifdef CHECK_MANIFEST $(CHECK_MANIFEST) endif ifdef SCSPELL $(SCSPELL) rstcheck.py setup.py README.rst endif test: ./test_rstcheck.py time ./test.bash readme: @restview --long-description --strict .PHONY: check test readme rstcheck-3.3.1/README.rst000066400000000000000000000151371337131466500150330ustar00rootroot00000000000000======== rstcheck ======== .. image:: https://travis-ci.org/myint/rstcheck.svg?branch=master :target: https://travis-ci.org/myint/rstcheck :alt: Build status Checks syntax of reStructuredText and code blocks nested within it. .. contents:: Installation ============ From pip:: $ pip install rstcheck Supported languages in code blocks ================================== - Bash - Doctest - C (C99) - C++ (C++11) - JSON - XML - Python - reStructuredText Examples ======== With bad Python syntax: .. code:: rst ==== Test ==== .. code:: python print( :: $ rstcheck bad_python.rst bad_python.rst:7: (ERROR/3) (python) unexpected EOF while parsing With bad C++ syntax: .. code:: rst ==== Test ==== .. code:: cpp int main() { return x; } :: $ rstcheck bad_cpp.rst bad_cpp.rst:9: (ERROR/3) (cpp) error: 'x' was not declared in this scope With bad syntax in the reStructuredText document itself: .. code:: rst ==== Test === :: $ rstcheck bad_rst.rst bad_rst.rst:1: (SEVERE/4) Title overline & underline mismatch. Options ======= :: usage: rstcheck [-h] [-r] [--report level] [--ignore-language language] [--ignore-messages messages] [--ignore-directives directives] [--ignore-substitutions substitutions] [--ignore-roles roles] [--debug] [--version] files [files ...] Checks code blocks in reStructuredText. positional arguments: files files to check optional arguments: -h, --help show this help message and exit -r, --recursive run recursively over directories --report level report system messages at or higher than level; info, warning, error, severe, none (default: info) --ignore-language language, --ignore language comma-separated list of languages to ignore --ignore-messages messages python regex that match the messages to ignore --ignore-directives directives comma-separated list of directives to ignore --ignore-substitutions substitutions comma-separated list of substitutions to ignore --ignore-roles roles comma-separated list of roles to ignore --debug show messages helpful for debugging --version show program's version number and exit Ignore specific languages ========================= You can ignore checking of nested code blocks by language. Either use the command-line option ``--ignore`` or put a comment in the document: .. code-block:: rst .. rstcheck: ignore-language=cpp,python,rst Ignore specific errors ====================== Since docutils doesn't categorize their error messages beyond the high-level categories of: info, warning, error, and severe; we need filter them out at a textual level. This is done by passing a Python regex. As example you can pass a regex like this to ignore several errors:: (Title underline too short.*|Duplicate implicit target.*') Configuration file ================== You can use the same arguments from the command line as options in the local configuration file of the project (just replace ``-`` for ``_``). ``rstcheck`` looks for a file ``.rstcheck.cfg`` in the directory or ancestor directory of the file it is checking. For example, consider a project with the following directory structure:: docs ├── foo │ └── bar.rst ├── index.rst └── .rstcheck.cfg ``.rstcheck.cfg`` contains: .. code-block:: cfg [rstcheck] ignore_directives=one,two,three ignore_roles=src,RFC ignore_messages=(Document or section may not begin with a transition\.$) report=warning ``bar.rst`` contains: .. code-block:: rst Bar === :src:`hello_world.py` :RFC:`793` .. one:: Hello ``rstcheck`` will make use of the ``.rstcheck.cfg``:: $ rstcheck docs/foo/bar.rst Sphinx ====== To enable Sphinx:: $ pip install sphinx The installed Sphinx version must be at least 1.5. To check that Sphinx support is enabled:: $ rstcheck -h | grep 'Sphinx is enabled' Usage in Vim ============ Using with Syntastic_: ---------------------- .. code:: vim let g:syntastic_rst_checkers = ['rstcheck'] Using with ALE_: ---------------- Just install ``rstcheck`` and make sure is on your path. .. _Syntastic: https://github.com/scrooloose/syntastic .. _ALE: https://github.com/w0rp/ale Use as a module =============== ``rstcheck.check()`` yields a series of tuples. The first value of each tuple is the line number (not the line index). The second value is the error message. >>> import rstcheck >>> list(rstcheck.check('Example\n===')) [(2, '(INFO/1) Possible title underline, too short for the title.')] Note that this does not load any configuration as that would mutate the ``docutils`` registries. Testing ======= To run all the tests, do:: $ make test Unit tests are in ``test_rstcheck.py``. System tests are composed of example good/bad input. The test inputs are contained in the ``examples`` directory. For basic tests, adding a test should just be a matter of adding files to ``examples/good`` or ``examples/bad``. History ======= 3.3.1 (2018-10-09) ------------------ - Make compatible with Sphinx >= 1.8. 3.3 (2018-03-17) ---------------- - Parse more options from configuration file (thanks to Santos Gallegos). - Allow ignoring specific (info/warning/error) messages via ``--ignore-messages`` (thanks to Santos Gallegos). 3.2 (2018-02-17) ---------------- - Check for invalid Markdown-style links (thanks to biscuitsnake). - Allow configuration to be stored in ``setup.cfg`` (thanks to Maël Pedretti). - Add ``--recursive`` option to recursively drill down directories to check for all ``*.rst`` files. 3.1 (2017-03-08) ---------------- - Add support for checking XML code blocks (thanks to Sameer Singh). 3.0.1 (2017-03-01) ------------------ - Support UTF-8 byte order marks (BOM). Previously, ``docutils`` would interpret the BOM as a visible character, which would lead to false positives about underlines being too short. 3.0 (2016-12-19) ---------------- - Optionally support Sphinx 1.5. Sphinx support will be enabled if Sphinx is installed. 2.0 (2015-07-27) ---------------- - Support loading settings from configuration files. 1.0 (2015-03-14) ---------------- - Add Sphinx support. 0.1 (2013-12-02) ---------------- - Initial version. .. rstcheck: ignore-language=cpp,python,rst rstcheck-3.3.1/examples/000077500000000000000000000000001337131466500151535ustar00rootroot00000000000000rstcheck-3.3.1/examples/bad/000077500000000000000000000000001337131466500157015ustar00rootroot00000000000000rstcheck-3.3.1/examples/bad/bad_bash.rst000066400000000000000000000000541337131466500201550ustar00rootroot00000000000000==== Test ==== .. code-block:: bash { rstcheck-3.3.1/examples/bad/bad_code.rst000066400000000000000000000001231337131466500201470ustar00rootroot00000000000000==== Test ==== ``code`` rather than ``code-block``. .. code:: python print( rstcheck-3.3.1/examples/bad/bad_cpp.rst000066400000000000000000000001221337131466500200160ustar00rootroot00000000000000==== Test ==== .. code-block:: cpp int main() { return x; } rstcheck-3.3.1/examples/bad/bad_markdown.rst000066400000000000000000000001001337131466500210520ustar00rootroot00000000000000==== Test ==== [Markdown-style Link](https://www.example.com/) rstcheck-3.3.1/examples/bad/bad_python.rst000066400000000000000000000000631337131466500205610ustar00rootroot00000000000000==== Test ==== .. code-block:: python print( rstcheck-3.3.1/examples/bad/bad_rst.rst000066400000000000000000000000161337131466500200460ustar00rootroot00000000000000==== Test === rstcheck-3.3.1/examples/bad/bad_rst_in_rst.rst000066400000000000000000000000711337131466500214250ustar00rootroot00000000000000==== Test ==== .. code-block:: rst Testing === rstcheck-3.3.1/examples/custom/000077500000000000000000000000001337131466500164655ustar00rootroot00000000000000rstcheck-3.3.1/examples/custom/good_with_custom.rst000066400000000000000000000000611337131466500225710ustar00rootroot00000000000000.. my-directive:: :some-custom-thing:`testing` rstcheck-3.3.1/examples/good/000077500000000000000000000000001337131466500161035ustar00rootroot00000000000000rstcheck-3.3.1/examples/good/bom.rst000066400000000000000000000003301337131466500174060ustar00rootroot00000000000000Byte order mark =============== docutils natively reports a false positive warning about the underline being to short if there is a BOM at the beginning of the title. https://github.com/myint/rstcheck/issues/23 rstcheck-3.3.1/examples/good/foo.h000066400000000000000000000000601337131466500170330ustar00rootroot00000000000000#ifndef FOO_H #define FOO_H int foo(); #endif rstcheck-3.3.1/examples/good/good.rst000066400000000000000000000035441337131466500175730ustar00rootroot00000000000000==== Test ==== .. code-block:: bash if [ "$x" == 'y' ] then exit 1 fi .. code-block:: c float foo(int n) { // Test C99. float x[n]; x[0] = 1; return x[0]; } .. code-block:: cpp #include int main() { auto x = 1; return x; } .. code-block:: python print(1) Run more tests for checking performance. .. code-block:: bash if [ "$x" == 'y' ] then exit 1 fi .. code-block:: c float foo(int n) { // Test C99. float x[n]; x[0] = 1; return x[0]; } .. code-block:: cpp #include int main() { auto x = 1; return x; } .. code-block:: python print(1) .. code-block:: bash if [ "$x" == 'y' ] then exit 1 fi .. code-block:: c float foo(int n) { // Test C99. float x[n]; x[0] = 1; return x[0]; } .. code-block:: cpp #include int main() { auto x = 1; return x; } .. code-block:: python print(1) .. code-block:: bash if [ "$x" == 'y' ] then exit 1 fi .. code-block:: c float foo(int n) { // Test C99. float x[n]; x[0] = 1; return x[0]; } .. code-block:: cpp #include int main() { auto x = 1; return x; } .. code-block:: python print(1) .. code-block:: bash if [ "$x" == 'y' ] then exit 1 fi .. code-block:: c float foo(int n) { // Test C99. float x[n]; x[0] = 1; return x[0]; } .. code-block:: cpp #include int main() { auto x = 1; return x; } .. code-block:: python # ¬∆˚ß∂ƒß∂ƒ˚¬∆ print(1) rstcheck-3.3.1/examples/good/good_cpp_with_local_include.rst000066400000000000000000000001541337131466500243370ustar00rootroot00000000000000==== Test ==== .. code-block:: cpp #include "foo.h" int main() { return foo(); } rstcheck-3.3.1/examples/good/good_markdown.rst000066400000000000000000000001361337131466500214670ustar00rootroot00000000000000==== Test ==== .. code-block:: markdown [Markdown-style Link](https://www.example.com/) rstcheck-3.3.1/examples/good/unicode.rst000066400000000000000000000002711337131466500202630ustar00rootroot00000000000000==== Тест ==== .. code-block:: python print('Привет!') .. code-block:: bash $ echo 'Привет' >> pipe.txt $ echo 'файловая труба!' >> pipe.txt rstcheck-3.3.1/examples/good/unknown.rst000066400000000000000000000000641337131466500203340ustar00rootroot00000000000000==== Test ==== File reeference: :file:`~/.bashrc`. rstcheck-3.3.1/examples/with_configuration/000077500000000000000000000000001337131466500210555ustar00rootroot00000000000000rstcheck-3.3.1/examples/with_configuration/.rstcheck.cfg000066400000000000000000000003071337131466500234220ustar00rootroot00000000000000[rstcheck] ignore_directives= foobar, my-directive, ignore_roles=some-custom-thing ignore_messages=(Document or section may not begin with a transition\.$) ignore_language=cpp report=warning rstcheck-3.3.1/examples/with_configuration/bad-2.rst000066400000000000000000000000611337131466500224710ustar00rootroot00000000000000-------- Duplicate ========= Subtitle -------- rstcheck-3.3.1/examples/with_configuration/bad.rst000066400000000000000000000001731337131466500223360ustar00rootroot00000000000000.. my-custom-directive:: :some-custom-thing:`testing` .. code-block:: cpp int main() { return x; } rstcheck-3.3.1/examples/with_configuration/good.rst000066400000000000000000000001641337131466500225400ustar00rootroot00000000000000.. my-directive:: :some-custom-thing:`testing` .. code-block:: cpp int main() { return x; } rstcheck-3.3.1/rstcheck.py000077500000000000000000000737471337131466500155420ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2013-2017 Steven Myint # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """Checks code blocks in reStructuredText.""" from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals import argparse import copy import contextlib import doctest import io import json import locale import multiprocessing import os import re import shutil import subprocess import sys import tempfile import xml.etree.ElementTree try: import configparser except ImportError: import ConfigParser as configparser import docutils.core import docutils.io import docutils.nodes import docutils.parsers.rst import docutils.utils import docutils.writers try: import sphinx SPHINX_INSTALLED = sphinx.version_info >= (1, 5) except (AttributeError, ImportError): SPHINX_INSTALLED = False if SPHINX_INSTALLED: import sphinx.application import sphinx.directives import sphinx.domains.c import sphinx.domains.cpp import sphinx.domains.javascript import sphinx.domains.python import sphinx.domains.std import sphinx.roles __version__ = '3.3.1' if SPHINX_INSTALLED: SPHINX_CODE_BLOCK_DELTA = -1 RSTCHECK_COMMENT_RE = re.compile(r'\.\. rstcheck:') # This is for the cases where code in a readme uses includes in that directory. INCLUDE_FLAGS = ['-I.', '-I..'] CONFIG_FILES = ['.rstcheck.cfg', 'setup.cfg'] class Error(Exception): """rstcheck exception.""" def __init__(self, message, line_number): self.line_number = line_number Exception.__init__(self, message) class CodeBlockDirective(docutils.parsers.rst.Directive): """Code block directive.""" has_content = True optional_arguments = 1 def run(self): """Run directive.""" try: language = self.arguments[0] except IndexError: language = '' code = '\n'.join(self.content) literal = docutils.nodes.literal_block(code, code) literal['classes'].append('code-block') literal['language'] = language return [literal] def register_code_directive(): """Register code directive.""" if not SPHINX_INSTALLED: docutils.parsers.rst.directives.register_directive('code', CodeBlockDirective) docutils.parsers.rst.directives.register_directive('code-block', CodeBlockDirective) docutils.parsers.rst.directives.register_directive('sourcecode', CodeBlockDirective) def strip_byte_order_mark(text): """Return text with byte order mark (BOM) removed.""" try: return text.encode('utf-8').decode('utf-8-sig') except UnicodeError: return text def check(source, filename='', report_level=docutils.utils.Reporter.INFO_LEVEL, ignore=None, debug=False): """Yield errors. Use lower report_level for noisier error output. Each yielded error is a tuple of the form: (line_number, message) Line numbers are indexed at 1 and are with respect to the full RST file. Each code block is checked asynchronously in a subprocess. Note that this function mutates state by calling the ``docutils`` ``register_*()`` functions. """ # Do this at call time rather than import time to avoid unnecessarily # mutating state. register_code_directive() ignore_sphinx() ignore = ignore or {} try: ignore.setdefault('languages', []).extend( find_ignored_languages(source) ) except Error as error: yield (error.line_number, '{}'.format(error)) writer = CheckWriter(source, filename, ignore=ignore) string_io = io.StringIO() # This is a hack to avoid false positive from docutils (#23). docutils # mistakes BOMs for actual visible letters. This results in the "underline # too short" warning firing. source = strip_byte_order_mark(source) try: docutils.core.publish_string( source, writer=writer, source_path=filename, settings_overrides={'halt_level': report_level, 'report_level': report_level, 'warning_stream': string_io}) except docutils.utils.SystemMessage: pass except AttributeError: # Sphinx will sometimes throw an exception trying to access # "self.state.document.settings.env". Ignore this for now until we # figure out a better approach. if debug: raise for checker in writer.checkers: for error in checker(): yield error rst_errors = string_io.getvalue().strip() if rst_errors: for message in rst_errors.splitlines(): try: ignore_regex = ignore.get('messages', '') if ignore_regex and re.search(ignore_regex, message): continue yield parse_gcc_style_error_message(message, filename=filename, has_column=False) except ValueError: continue def find_ignored_languages(source): """Yield ignored languages. Languages are ignored via comment. For example, to ignore C++, JSON, and Python: >>> list(find_ignored_languages(''' ... Example ... ======= ... ... .. rstcheck: ignore-language=cpp,json ... ... .. rstcheck: ignore-language=python ... ''')) ['cpp', 'json', 'python'] """ for (index, line) in enumerate(source.splitlines()): match = RSTCHECK_COMMENT_RE.match(line) if match: key_and_value = line[match.end():].strip().split('=') if len(key_and_value) != 2: raise Error('Expected "key=value" syntax', line_number=index + 1) if key_and_value[0] == 'ignore-language': for language in key_and_value[1].split(','): yield language.strip() def _check_file(parameters): """Return list of errors.""" (filename, args) = parameters if filename == '-': contents = sys.stdin.read() else: with contextlib.closing( docutils.io.FileInput(source_path=filename) ) as input_file: contents = input_file.read() args = load_configuration_from_file( os.path.dirname(os.path.realpath(filename)), args) ignore_directives_and_roles(args.ignore_directives, args.ignore_roles) for substitution in args.ignore_substitutions: contents = contents.replace('|{}|'.format(substitution), 'None') ignore = { 'languages': args.ignore_language, 'messages': args.ignore_messages, } all_errors = [] for error in check(contents, filename=filename, report_level=args.report, ignore=ignore, debug=args.debug): all_errors.append(error) return (filename, all_errors) def check_python(code): """Yield errors.""" try: compile(code, '', 'exec') except SyntaxError as exception: yield (int(exception.lineno), exception.msg) def check_json(code): """Yield errors.""" try: json.loads(code) except ValueError as exception: message = '{}'.format(exception) line_number = 0 found = re.search(r': line\s+([0-9]+)[^:]*$', message) if found: line_number = int(found.group(1)) yield (int(line_number), message) def check_xml(code): """Yield errors.""" try: xml.etree.ElementTree.fromstring(code) except xml.etree.ElementTree.ParseError as exception: message = '{}'.format(exception) line_number = 0 found = re.search(r': line\s+([0-9]+)[^:]*$', message) if found: line_number = int(found.group(1)) yield (int(line_number), message) def check_rst(code, ignore): """Yield errors in nested RST code.""" filename = '' for result in check(code, filename=filename, ignore=ignore): yield result def check_doctest(code): """Yield doctest syntax errors. This does not run the test as that would be unsafe. Nor does this check the Python syntax in the doctest. That could be purposely incorrect for testing purposes. """ parser = doctest.DocTestParser() try: parser.parse(code) except ValueError as exception: message = '{}'.format(exception) match = re.match('line ([0-9]+)', message) if match: yield (int(match.group(1)), message) def get_and_split(options, key, default=''): """Return list of split and stripped strings.""" return split_comma_separated(options.get(key, default)) def split_comma_separated(text): """Return list of split and stripped strings.""" return [t.strip() for t in text.split(',') if t.strip()] def _get_directives_and_roles_from_sphinx(): """Return a tuple of Sphinx directive and roles.""" if SPHINX_INSTALLED: sphinx_directives = list(sphinx.domains.std.StandardDomain.directives) sphinx_roles = list(sphinx.domains.std.StandardDomain.roles) for domain in [sphinx.domains.c.CDomain, sphinx.domains.cpp.CPPDomain, sphinx.domains.javascript.JavaScriptDomain, sphinx.domains.python.PythonDomain]: sphinx_directives += list(domain.directives) + [ '{}:{}'.format(domain.name, item) for item in list(domain.directives)] sphinx_roles += list(domain.roles) + [ '{}:{}'.format(domain.name, item) for item in list(domain.roles)] else: sphinx_roles = [ 'abbr', 'command', 'dfn', 'doc', 'download', 'envvar', 'file', 'guilabel', 'kbd', 'keyword', 'mailheader', 'makevar', 'manpage', 'menuselection', 'mimetype', 'newsgroup', 'option', 'program', 'py:func', 'ref', 'regexp', 'samp', 'term', 'token'] sphinx_directives = [ 'autosummary', 'currentmodule', 'centered', 'c:function', 'c:type', 'include', 'deprecated', 'envvar', 'glossary', 'index', 'no-code-block', 'literalinclude', 'hlist', 'option', 'productionlist', 'py:function', 'seealso', 'toctree', 'todo', 'versionadded', 'versionchanged'] return (sphinx_directives, sphinx_roles) class IgnoredDirective(docutils.parsers.rst.Directive): """Stub for unknown directives.""" has_content = True def run(self): """Do nothing.""" return [] def _ignore_role(name, rawtext, text, lineno, inliner, options=None, content=None): """Stub for unknown roles.""" # pylint: disable=unused-argument return ([], []) def ignore_sphinx(): """Register Sphinx directives and roles to ignore.""" (directives, roles) = _get_directives_and_roles_from_sphinx() directives += [ 'centered', 'include', 'deprecated', 'index', 'no-code-block', 'literalinclude', 'hlist', 'seealso', 'toctree', 'todo', 'versionadded', 'versionchanged'] ext_autosummary = [ 'autosummary', 'currentmodule', ] ignore_directives_and_roles(directives + ext_autosummary, roles + ['ctype']) def find_config(directory): """Return configuration filename. Find configuration in directory or its ancestor. """ directory = os.path.realpath(directory) while directory: for filename in CONFIG_FILES: candidate = os.path.join(directory, filename) if os.path.exists(candidate): return candidate parent_directory = os.path.dirname(directory) if parent_directory == directory: break else: directory = parent_directory def load_configuration_from_file(directory, args): """Return new ``args`` with configuration loaded from file.""" args = copy.copy(args) options = _get_options(directory) args.report = options.get('report', args.report) threshold_dictionary = docutils.frontend.OptionParser.thresholds args.report = int(threshold_dictionary.get(args.report, args.report)) args.ignore_language = get_and_split( options, 'ignore_language', args.ignore_language) args.ignore_messages = options.get( 'ignore_messages', args.ignore_messages) args.ignore_directives = get_and_split( options, 'ignore_directives', args.ignore_directives) args.ignore_substitutions = get_and_split( options, 'ignore_substitutions', args.ignore_substitutions) args.ignore_roles = get_and_split( options, 'ignore_roles', args.ignore_roles) return args def _get_options(directory): config_path = find_config(directory) if not config_path: return {} parser = configparser.ConfigParser() parser.read(config_path) try: options = dict(parser.items('rstcheck')) except configparser.NoSectionError: return {} else: return options def ignore_directives_and_roles(directives, roles): """Ignore directives/roles in docutils.""" for directive in directives: docutils.parsers.rst.directives.register_directive(directive, IgnoredDirective) for role in roles: docutils.parsers.rst.roles.register_local_role(role, _ignore_role) # The checker functions below return a checker. This is for purposes of # asynchronous checking. As we visit each code block, a subprocess gets # launched to run the checker. They all run in the background until we finish # traversing the document. At that point, we accumulate the errors. def bash_checker(code, working_directory): """Return checker.""" run = run_in_subprocess(code, '.bash', ['bash', '-n'], working_directory=working_directory) def run_check(): """Yield errors.""" result = run() if result: (output, filename) = result prefix = filename + ': line ' for line in output.splitlines(): if not line.startswith(prefix): continue message = line[len(prefix):] split_message = message.split(':', 1) yield (int(split_message[0]) - 1, split_message[1].strip()) return run_check def c_checker(code, working_directory): """Return checker.""" return gcc_checker(code, '.c', [os.getenv('CC', 'gcc'), '-std=c99'] + INCLUDE_FLAGS, working_directory=working_directory) def cpp_checker(code, working_directory): """Return checker.""" return gcc_checker(code, '.cpp', [os.getenv('CXX', 'g++'), '-std=c++0x'] + INCLUDE_FLAGS, working_directory=working_directory) def gcc_checker(code, filename_suffix, arguments, working_directory): """Return checker.""" run = run_in_subprocess(code, filename_suffix, arguments + ['-pedantic', '-fsyntax-only'], working_directory=working_directory) def run_check(): """Yield errors.""" result = run() if result: (output, filename) = result for line in output.splitlines(): try: yield parse_gcc_style_error_message(line, filename=filename) except ValueError: continue return run_check def parse_gcc_style_error_message(message, filename, has_column=True): """Parse GCC-style error message. Return (line_number, message). Raise ValueError if message cannot be parsed. """ colons = 2 if has_column else 1 prefix = filename + ':' if not message.startswith(prefix): raise ValueError() message = message[len(prefix):] split_message = message.split(':', colons) line_number = int(split_message[0]) return (line_number, split_message[colons].strip()) def get_encoding(): """Return preferred encoding.""" return locale.getpreferredencoding() or sys.getdefaultencoding() def run_in_subprocess(code, filename_suffix, arguments, working_directory): """Return None on success.""" temporary_file = tempfile.NamedTemporaryFile(mode='wb', suffix=filename_suffix) temporary_file.write(code.encode('utf-8')) temporary_file.flush() process = subprocess.Popen(arguments + [temporary_file.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=working_directory) def run(): """Yield errors.""" raw_result = process.communicate() if process.returncode != 0: return (raw_result[1].decode(get_encoding()), temporary_file.name) return run class CheckTranslator(docutils.nodes.NodeVisitor): """Visits code blocks and checks for syntax errors in code.""" def __init__(self, document, contents, filename, ignore): docutils.nodes.NodeVisitor.__init__(self, document) self.checkers = [] self.contents = contents self.filename = filename self.working_directory = os.path.dirname(os.path.realpath(filename)) self.ignore = ignore or {} self.ignore.setdefault('languages', []).append(None) def visit_doctest_block(self, node): """Check syntax of doctest.""" if 'doctest' in self.ignore['languages']: return self._add_check(node=node, run=lambda: check_doctest(node.rawsource), language='doctest', is_code_node=False) def visit_literal_block(self, node): """Check syntax of code block.""" # For "..code-block:: language" language = node.get('language', None) is_code_node = False if not language: # For "..code:: language" is_code_node = True classes = node.get('classes') if 'code' in classes: language = classes[-1] else: return if language in self.ignore['languages']: return if language == 'doctest' or ( language == 'python' and node.rawsource.lstrip().startswith('>>> ')): self.visit_doctest_block(node) raise docutils.nodes.SkipNode checker = { 'bash': bash_checker, 'c': c_checker, 'cpp': cpp_checker, 'json': lambda source, _: lambda: check_json(source), 'xml': lambda source, _: lambda: check_xml(source), 'python': lambda source, _: lambda: check_python(source), 'rst': lambda source, _: lambda: check_rst(source, ignore=self.ignore) }.get(language) if checker: run = checker(node.rawsource, self.working_directory) self._add_check(node=node, run=run, language=language, is_code_node=is_code_node) raise docutils.nodes.SkipNode def visit_paragraph(self, node): """Check syntax of reStructuredText.""" find = re.search(r'\[[^\]]+\]\([^\)]+\)', node.rawsource) if find is not None: self.document.reporter.warning( '(rst) Link is formatted in Markdown style.', base_node=node) def _add_check(self, node, run, language, is_code_node): """Add checker that will be run.""" def run_check(): """Yield errors.""" all_results = run() if all_results is not None: if all_results: for result in all_results: error_offset = result[0] - 1 line_number = getattr(node, 'line', None) if line_number is not None: yield ( beginning_of_code_block( node=node, line_number=line_number, full_contents=self.contents, is_code_node=is_code_node) + error_offset, '({}) {}'.format(language, result[1])) else: yield (self.filename, 0, 'unknown error') self.checkers.append(run_check) def unknown_visit(self, node): """Ignore.""" def unknown_departure(self, node): """Ignore.""" def beginning_of_code_block(node, line_number, full_contents, is_code_node): """Return line number of beginning of code block.""" if SPHINX_INSTALLED and not is_code_node: delta = len(node.non_default_attributes()) current_line_contents = full_contents.splitlines()[line_number:] blank_lines = next( (i for (i, x) in enumerate(current_line_contents) if x), 0) return ( line_number + delta - 1 + blank_lines - 1 + SPHINX_CODE_BLOCK_DELTA) else: lines = full_contents.splitlines() code_block_length = len(node.rawsource.splitlines()) try: # Case where there are no extra spaces. if lines[line_number - 1].strip(): return line_number - code_block_length + 1 except IndexError: pass # The offsets are wrong if the RST text has multiple blank lines after # the code block. This is a workaround. for line_number in range(line_number, 1, -1): if lines[line_number - 2].strip(): break return line_number - code_block_length class CheckWriter(docutils.writers.Writer): """Runs CheckTranslator on code blocks.""" def __init__(self, contents, filename, ignore): docutils.writers.Writer.__init__(self) self.checkers = [] self.contents = contents self.filename = filename self.ignore = ignore def translate(self): """Run CheckTranslator.""" visitor = CheckTranslator(self.document, contents=self.contents, filename=self.filename, ignore=self.ignore) self.document.walkabout(visitor) self.checkers += visitor.checkers def decode_filename(filename): """Return Unicode filename.""" if hasattr(filename, 'decode'): return filename.decode(sys.getfilesystemencoding()) else: return filename def parse_args(): """Return parsed command-line arguments.""" threshold_choices = docutils.frontend.OptionParser.threshold_choices parser = argparse.ArgumentParser( description=__doc__ + (' Sphinx is enabled.' if SPHINX_INSTALLED else ''), prog='rstcheck') parser.add_argument('files', nargs='+', type=decode_filename, help='files to check') parser.add_argument('-r', '--recursive', action='store_true', help='run recursively over directories') parser.add_argument('--report', metavar='level', choices=threshold_choices, default='info', help='report system messages at or higher than ' 'level; ' + ', '.join(choice for choice in threshold_choices if not choice.isdigit()) + ' (default: %(default)s)') parser.add_argument('--ignore-language', '--ignore', metavar='language', default='', help='comma-separated list of languages to ignore') parser.add_argument('--ignore-messages', metavar='messages', default='', help='python regex that match the messages to ignore') parser.add_argument('--ignore-directives', metavar='directives', default='', help='comma-separated list of directives to ignore') parser.add_argument('--ignore-substitutions', metavar='substitutions', default='', help='comma-separated list of substitutions to ignore') parser.add_argument('--ignore-roles', metavar='roles', default='', help='comma-separated list of roles to ignore') parser.add_argument('--debug', action='store_true', help='show messages helpful for debugging') parser.add_argument('--version', action='version', version='%(prog)s ' + __version__) args = parser.parse_args() if '-' in args.files: if len(args.files) > 1: parser.error("'-' for standard in can only be checked alone") else: args.files = list(find_files(filenames=args.files, recursive=args.recursive)) return args def output_message(text, file=sys.stderr): """Output message to terminal.""" if file.encoding is None: # If the output file does not support Unicode, encode it to a byte # string. On some machines, this occurs when Python is redirecting to # file (or piping to something like Vim). text = text.encode('utf-8') print(text, file=file) @contextlib.contextmanager def enable_sphinx_if_possible(): """Register Sphinx directives and roles.""" if SPHINX_INSTALLED: srcdir = tempfile.mkdtemp() outdir = os.path.join(srcdir, '_build') try: sphinx.application.Sphinx(srcdir=srcdir, confdir=None, outdir=outdir, doctreedir=outdir, buildername='dummy', status=None) yield finally: shutil.rmtree(srcdir) else: yield def match_file(filename): """Return True if file is okay for modifying/recursing.""" base_name = os.path.basename(filename) if base_name.startswith('.'): return False if not os.path.isdir(filename) and not filename.lower().endswith('.rst'): return False return True def find_files(filenames, recursive): """Yield filenames.""" while filenames: name = filenames.pop(0) if recursive and os.path.isdir(name): for root, directories, children in os.walk(name): filenames += [os.path.join(root, f) for f in children if match_file(os.path.join(root, f))] directories[:] = [d for d in directories if match_file(os.path.join(root, d))] else: yield name def main(): """Return 0 on success.""" args = parse_args() if not args.files: return 0 with enable_sphinx_if_possible(): status = 0 pool = multiprocessing.Pool(multiprocessing.cpu_count()) try: if len(args.files) > 1: results = pool.map( _check_file, [(name, args) for name in args.files]) else: # This is for the case where we read from standard in. results = [_check_file((args.files[0], args))] for (filename, errors) in results: for error in errors: line_number = error[0] message = error[1] if not re.match(r'\([A-Z]+/[0-9]+\)', message): message = '(ERROR/3) ' + message output_message('{}:{}: {}'.format(filename, line_number, message)) status = 1 except (IOError, UnicodeError) as exception: output_message(exception) status = 1 return status if __name__ == '__main__': sys.exit(main()) rstcheck-3.3.1/setup.py000077500000000000000000000026071337131466500150570ustar00rootroot00000000000000#!/usr/bin/env python """Installer for rstcheck.""" import ast import io import setuptools def version(): """Return version string.""" with io.open('rstcheck.py', encoding='utf-8') as input_file: for line in input_file: if line.startswith('__version__'): return ast.parse(line).body[0].value.s with io.open('README.rst', encoding='utf-8') as readme: setuptools.setup( name='rstcheck', version=version(), url='https://github.com/myint/rstcheck', description='Checks syntax of reStructuredText and code blocks nested ' 'within it', long_description=readme.read(), classifiers=[ 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Software Development :: Quality Assurance', ], keywords='restructuredtext,lint,check,pypi,readme,rst,analyze', py_modules=['rstcheck'], entry_points={'console_scripts': ['rstcheck = rstcheck:main']}, install_requires=['docutils >= 0.7']) rstcheck-3.3.1/test.bash000077500000000000000000000031631337131466500151610ustar00rootroot00000000000000#!/bin/bash # System tests. set -eux trap "echo -e '\x1b[01;31mFailed\x1b[0m'" ERR for name in examples/good/*.rst do ./rstcheck.py "$name" done for name in examples/bad/*.rst do if ./rstcheck.py "$name" then exit 1 fi done # Test multiple files. ./rstcheck.py examples/good/*.rst ./rstcheck.py \ --ignore-directives=my-directive \ --ignore-role=some-custom-thing \ examples/custom/good_with_custom.rst ./rstcheck.py --ignore-language=cpp examples/bad/bad_cpp.rst ./rstcheck.py - < examples/good/good.rst # Test multiple mix of good/bad files. if ./rstcheck.py examples/bad/bad_cpp.rst examples/good/good.rst then exit 1 fi # "-" should only be allowed to be checked alone. if ./rstcheck.py - examples/good/good.rst then exit 1 fi ./rstcheck.py --report=none examples/bad/bad_rst.rst if ./rstcheck.py missing_file.rst then exit 1 fi ./rstcheck.py --recursive examples/good if ./rstcheck.py --recursive examples/bad then exit 1 fi # Test ignore messages ./rstcheck.py examples/bad/bad_rst.rst --ignore-messages '(Title .verline & underline mismatch\.$)' if ./rstcheck.py examples/bad/bad_rst.rst --ignore-messages '(No match\.$)' then exit 1 fi # Test configuration file ./rstcheck.py examples/with_configuration/good.rst if ./rstcheck.py examples/with_configuration/bad.rst then exit 1 fi # Ignore message on configuration file ./rstcheck.py examples/with_configuration/bad-2.rst if python -c 'import sys; sys.exit(0 if sys.version_info >= (3,) else 1)' then python -m doctest -v README.rst rstcheck.py ./rstcheck.py README.rst fi echo -e '\x1b[01;32mOkay\x1b[0m' rstcheck-3.3.1/test_rstcheck.py000077500000000000000000000172631337131466500165700ustar00rootroot00000000000000#!/usr/bin/env python """Test suite for rstcheck.""" from __future__ import unicode_literals import unittest import rstcheck # We don't do this in the module itself to avoid mutation. rstcheck.ignore_sphinx() class Tests(unittest.TestCase): def assert_lines_equal(self, line_numbers, results): self.assertEqual(set(line_numbers), set(dict(results))) def test_parse_gcc_style_error_message(self): self.assertEqual( (32, 'error message'), rstcheck.parse_gcc_style_error_message( 'filename:32:7: error message', filename='filename')) def test_parse_gcc_style_error_message_with_no_column(self): self.assertEqual( (32, 'error message'), rstcheck.parse_gcc_style_error_message( 'filename:32: error message', filename='filename', has_column=False)) def test_parse_gcc_style_error_message_with_parsing_error(self): with self.assertRaises(ValueError): rstcheck.parse_gcc_style_error_message( ':32:3 error message', filename='filename') with self.assertRaises(IndexError): rstcheck.parse_gcc_style_error_message( 'filename:32: error message', filename='filename', has_column=True) def test_check(self): self.assert_lines_equal( [6], rstcheck.check( """\ Test ==== .. code:: python print( """)) def test_check_code_block(self): self.assert_lines_equal( [6], rstcheck.check( """\ Test ==== .. code-block:: python print( """)) def test_check_json(self): self.assert_lines_equal( [7], rstcheck.check( """\ Test ==== .. code-block:: json { 'abc': 123 } """)) def test_check_json_with_ignore(self): self.assert_lines_equal( [], rstcheck.check( """\ Test ==== .. code-block:: json { 'abc': 123 } .. rstcheck: ignore-language=json,python,rst """)) def test_check_json_with_unmatched_ignores_only(self): self.assert_lines_equal( [7], rstcheck.check( """\ Test ==== .. code-block:: json { 'abc': 123 } .. rstcheck: ignore-language=cpp,python,rst """)) def test_check_json_with_bad_ignore(self): self.assert_lines_equal( [7, 10], rstcheck.check( """\ Test ==== .. code-block:: json { 'abc': 123 } .. rstcheck: ignore-language json,python,rst """)) def test_check_xml(self): self.assert_lines_equal( [8], rstcheck.check( """\ Test ==== .. code-block:: xml 123 """)) def test_check_xml_with_ignore(self): self.assert_lines_equal( [], rstcheck.check( """\ Test ==== .. code-block:: xml 123 .. rstcheck: ignore-language=xml,python,rst """)) def test_check_xml_with_unmatched_ignores_only(self): self.assert_lines_equal( [8], rstcheck.check( """\ Test ==== .. code-block:: xml 123 .. rstcheck: ignore-language=cpp,python,rst """)) def test_check_xml_with_bad_ignore(self): self.assert_lines_equal( [8, 11], rstcheck.check( """\ Test ==== .. code-block:: xml 123 .. rstcheck: ignore-language xml,python,rst """)) def test_check_with_extra_blank_lines_before(self): self.assert_lines_equal( [8], rstcheck.check( """\ Test ==== .. code-block:: python print( """)) def test_check_with_extra_blank_lines_after(self): self.assert_lines_equal( [6], rstcheck.check( """\ Test ==== .. code-block:: python print( """)) def test_check_with_extra_blank_lines_before_and_after(self): self.assert_lines_equal( [8], rstcheck.check( """\ Test ==== .. code-block:: python print( """)) def test_check_rst(self): self.assert_lines_equal( [2], rstcheck.check( """\ Test === """)) def test_check_rst_report_level(self): self.assert_lines_equal( [], rstcheck.check( """\ Test === """, report_level=5)) def test_check_nested_rst(self): self.assert_lines_equal( [32], rstcheck.check( """\ Test ==== .. code-block:: rst Test ==== .. code-block:: rst Test ==== .. code-block:: rst Test ==== .. code-block:: rst Test ==== .. code-block:: rst Test ==== .. code-block:: python print( """)) @unittest.skipIf(not rstcheck.SPHINX_INSTALLED, 'Requires Sphinx') def test_ignore_sphinx_directives(self): self.assert_lines_equal( [], rstcheck.check( """\ .. toctree:: :maxdepth: 2 intro strings datatypes numeric (many more documents listed here) .. highlight:: python :linenothreshold: 5 :: print('Hello') .. code-block:: ruby :linenos: puts "Hello!" .. code-block:: python :linenos: :emphasize-lines: 3,5 def some_function(): interesting = False print('This line is highlighted.') print('This one is not...') print('...but this one is.') .. literalinclude:: rstcheck.py :language: python :linenos: """)) def test_check_doctest(self): self.assert_lines_equal( [5], rstcheck.check( """\ Testing ======= >>> x = 1 >>>> x 1 """)) def test_check_doctest_do_not_crash_when_indented(self): """docutils does not provide line number when indented.""" list(rstcheck.check( """\ Testing ======= >>> x = 1 >>>> x 1 """)) def test_check_doctest_with_ignore(self): self.assert_lines_equal( [], rstcheck.check( """\ Testing ======= >>> x = 1 >>>> x 1 .. rstcheck: ignore-language=doctest """)) @unittest.skipIf(rstcheck.SPHINX_INSTALLED, 'Does not work with Sphinx') def test_check_doctest_in_code(self): self.assert_lines_equal( [7], rstcheck.check( """\ Testing ======= .. code:: doctest >>> x = 1 >>>> x 1 """)) def test_check_doctest_in_code_block(self): self.assert_lines_equal( [7], rstcheck.check( """\ Testing ======= .. code-block:: doctest >>> x = 1 >>>> x 1 """)) def test_check_doctest_in_python_code_block(self): """I'm not sure if this is correct, but I've seen people do it.""" self.assert_lines_equal( [7], rstcheck.check( """\ Testing ======= .. code-block:: python >>> x = 1 >>>> x 1 """)) def main(): with rstcheck.enable_sphinx_if_possible(): unittest.main() if __name__ == '__main__': main()