pax_global_header00006660000000000000000000000064134455273620014525gustar00rootroot0000000000000052 comment=20cc0fb4bdfc5291df8880056d70290f2d1ad8c2 .foundryrc000066400000000000000000000003221344552736200131020ustar00rootroot00000000000000{ "releaseCommands": [ { "type": "customCommand", "updateFiles": "echo \"$FOUNDRY_VERSION\" > restructuredtext_lint/VERSION" }, "foundry-release-git", "foundry-release-pypi" ] } .gitignore000066400000000000000000000001001344552736200130500ustar00rootroot00000000000000*.pyc build/ dist/ restructuredtext_lint.egg-info node_modules/ .travis.yml000066400000000000000000000006021344552736200132000ustar00rootroot00000000000000language: python dist: xenial python: - "2.7" - "3.4" - "3.5" - "3.6" - "3.7" install: # Install `restructuredtext_lint` - python setup.py develop # Install dev requirements - pip install -r requirements-dev.txt script: # Run our tests - ./test.sh notifications: email: recipients: - todd@twolfson.com on_success: change on_failure: change CHANGELOG.rst000066400000000000000000000055151344552736200131200ustar00rootroot00000000000000restructuredtext-lint changelog =============================== 1.3.0 - Added Python 3.7 support via @Jenselme in #47 1.2.2 - Dropped PyYAML dev dependency to fix GitHub vulnerability warning 1.2.1 - Dropped Python 3.3 from Travis CI to fix testing errors 1.2.0 - Added directory support via @dhruvsomani in #48. Fixes #38 1.1.3 - Updated documentation and typos via @jwilk in #44 and #45 1.1.2 - Replaced Gittip with support me page 1.1.1 - Removed PyPy3 from Travis CI to fix failures 1.1.0 - Added support for `--rst-prolog`. Fixes #39 1.0.1 - Repaired Python 3 testing errors in Travis CI 1.0.0 - Moved to "Silence is golden" philosophy. Clean files now have no output 0.18.0 - Added `--level` CLI option via @peterjc in #37 0.17.2 - Added `zip_safe` flag via @djanderson in #35 0.17.1 - Updated documentation with new info on Sphinx from #29 0.17.0 - Added back ``--version`` support with missing ``include_package_data`` flag 0.16.0 - Reverted support for ``--version`` due to breaking installation from PyPI 0.15.0 - Added support for ``--version`` 0.14.3 - Moved ``with`` statement for opening/closing files to prevent leaking file descriptors via @asottile in #28 0.14.2 - Documented common PyPI issues 0.14.1 - Fixed up indentation annoyances in README 0.14.0 - Repaired JSON with no errors via @Iyozhikov in #25 0.13.0 - Added absolute imports to repair local development 0.12.4 - Added CPython@3.4, CPython@3.5, and PyPy@3 to Travis CI tests 0.12.3 - Added ``foundry`` for release 0.12.2 - Repaired imports to be absolute within package to fix Python3 issues via @fizyk in #21 0.12.1 - Added ``flake8-quotes`` to lint for non-single quotes 0.12.0 - Added support for multiple ``.rst`` files to ``rst-lint`` command via @pwilczynskiclearcode in #19 0.11.3 - Documented added in new directives/roles 0.11.2 - Added test failures for lint errors 0.11.1 - Fixed bad assertions in tests 0.11.0 - Added fix for errors that have no line number (e.g. invalid links). Fixes #12 0.10.0 - Remerged change from ``0.8.0`` to loosen ``docutils`` dependency to allow minor fluctuations. Fixes #9 0.9.0 - Added ``flake8`` linting via @berendt in #10 0.8.0 - Loosened ``docutils`` dependency to allow minor fluctuations. Fixes #9 0.7.0 - Increased ``halt_level`` to 5 (never halt) to collect all errors. Fixes #7 0.6.0 - Rewrote library to be inline with ``rst2html.py`` flow. Added error collecting on transforms. Fixes #6 0.5.0 - Relocated tests to follow convention and make running specific tests easier 0.4.0 - Repaired regression for bad parses that did not return all promised data (e.g. line number). Fixes #5 0.3.1 - Documented ``lint_file`` method 0.3.0 - Moved from ``read`` to ``io.read``, allowing specification of ``encoding`` from CLI. Attribution to @coldfix 0.2.0 - Added CLI utility 0.1.1 - Repairing link for PyPI 0.1.0 - Initial release MANIFEST.in000066400000000000000000000000641344552736200126270ustar00rootroot00000000000000recursive-include restructuredtext_lint * include * README.rst000066400000000000000000000173261344552736200125710ustar00rootroot00000000000000restructuredtext-lint ===================== .. image:: https://travis-ci.org/twolfson/restructuredtext-lint.png?branch=master :target: https://travis-ci.org/twolfson/restructuredtext-lint :alt: Build Status `reStructuredText`_ `linter`_ This was created out of frustration with `PyPI`_; it sucks finding out your `reST`_ is invalid **after** uploading it. It is being developed in junction with a `Sublime Text`_ linter. .. _`reStructuredText`: http://docutils.sourceforge.net/rst.html .. _`linter`: http://en.wikipedia.org/wiki/Lint_%28software%29 .. _`reST`: `reStructuredText`_ .. _`PyPI`: http://pypi.python.org/ .. _`Sublime Text`: http://sublimetext.com/ Getting Started --------------- Install the module with: ``pip install restructuredtext_lint`` .. code:: python import restructuredtext_lint errors = restructuredtext_lint.lint(""" Hello World ======= """) # `errors` will be list of system messages # [>] errors[0].message # Title underline too short. CLI Utility ^^^^^^^^^^^ For your convenience, we present a CLI utility ``rst-lint`` (also available as ``restructuredtext-lint``). .. code:: console $ rst-lint --help usage: rst-lint [-h] [--version] [--format {text,json}] [--encoding ENCODING] [--level {debug,info,warning,error,severe}] [--rst-prolog RST_PROLOG] path [path ...] Lint reStructuredText files. Returns 0 if all files pass linting, 1 for an internal error, and 2 if linting failed. positional arguments: path File/folder to lint optional arguments: -h, --help show this help message and exit --version show program's version number and exit --format {text,json} Format of the output (default: "text") --encoding ENCODING Encoding of the input file (e.g. "utf-8") --level {debug,info,warning,error,severe} Minimum error level to report (default: "warning") --rst-prolog RST_PROLOG reStructuredText content to prepend to all files (useful for substitutions) $ rst-lint README.rst WARNING README.rst:2 Title underline too short. PyPI issues ^^^^^^^^^^^ While a document may lint cleanly locally, there can be issues when submitted it to `PyPI`_. Here are some common problems: - Usage of non-builtin lexers (e.g. ``bibtex``) will pass locally but not be recognized/parsable on `PyPI`_ - This is due to `PyPI`_ not having a non-builtin lexer installed - Please avoid non-builtin lexers to avoid complications - For more information, see `#27`_ - Relative hyperlinks will not work (e.g. ``./UNLICENSE``) - According to Stack Overflow, hyperlinks must use a scheme (e.g. ``http``, ``https``) and that scheme must be whitelisted - http://stackoverflow.com/a/16594755 - Please use absolute hyperlinks (e.g. ``https://github.com/twolfson/restructuredtext-lint/blob/master/UNLICENSE``) .. _`#27`: https://github.com/twolfson/restructuredtext-lint/issues/27 Documentation ------------- ``restructuredtext-lint`` exposes a ``lint`` and ``lint_file`` function ``restructuredtext_lint.lint(content, filepath=None, rst_prolog=None)`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Lint `reStructuredText`_ and return errors - content ``String`` - `reStructuredText`_ to be linted - filepath ``String`` - Optional path to file, this will be returned as the source - rst_prolog ``String`` - Optional content to prepend to content, line numbers will be offset to ignore this Returns: - errors ``List`` - List of errors - Each error is a class from `docutils`_ with the following attrs - line ``Integer|None`` - Line where the error occurred - On rare occasions, this will be ``None`` (e.g. anonymous link mismatch) - source ``String`` - ``filepath`` provided in parameters - level ``Integer`` - Level of the warning - Levels represent 'info': 1, 'warning': 2, 'error': 3, 'severe': 4 - type ``String`` - Noun describing the error level - Levels can be 'INFO', 'WARNING', 'ERROR', or 'SEVERE' - message ``String`` - Error message - full_message ``String`` - Error message and source lines where the error occurred - It should be noted that ``level``, ``type``, ``message``, and ``full_message`` are custom attrs added onto the original ``system_message`` .. _`docutils`: http://docutils.sourceforge.net/ ``restructuredtext_lint.lint_file(filepath, encoding=None, *args, **kwargs)`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Lint a `reStructuredText`_ file and return errors - filepath ``String`` - Path to file for linting - encoding ``String`` - Encoding to read file in as - When ``None`` is provided, it will use OS default as provided by `locale.getpreferredencoding`_ - The list of supported encodings can be found at http://docs.python.org/2/library/codecs.html#standard-encodings - ``*args`` - Additional arguments to be passed to ``lint`` - ``**kwargs`` - Additional keyword arguments to be passed to ``lint`` .. _`locale.getpreferredencoding`: http://docs.python.org/2/library/locale.html#locale.getpreferredencoding Returns: Same structure as ``restructuredtext_lint.lint`` Extension --------- Under the hood, we leverage `docutils`_ for parsing reStructuredText documents. `docutils`_ supports adding new directives and roles via ``register_directive`` and ``register_role``. Sphinx ^^^^^^ Unfortunately due to customizations in `Sphinx's parser`_ we cannot include all of its directives/roles (see `#29`_). However, we can include some of them as one-offs. Here is an example of adding a directive from `Sphinx`_. .. _`Sphinx`: http://sphinx-doc.org/ .. _`Sphinx's parser`: Sphinx_ .. _`#29`: https://github.com/twolfson/restructuredtext-lint/issues/29#issuecomment-243456787 https://github.com/sphinx-doc/sphinx/blob/1.3/sphinx/directives/code.py **sphinx.rst** .. code:: rst Hello ===== World .. highlight:: python Hello World! **sphinx.py** .. code:: python # Load in our dependencies from docutils.parsers.rst.directives import register_directive from sphinx.directives.code import Highlight import restructuredtext_lint # Load our new directive register_directive('highlight', Highlight) # Lint our README errors = restructuredtext_lint.lint_file('docs/sphinx/README.rst') print errors[0].message # Error in "highlight" directive: no content permitted. Examples -------- Here is an example of all invalid properties .. code:: python rst = """ Some content. Hello World ======= Some more content! """ errors = restructuredtext_lint.lint(rst, 'myfile.py') errors[0].line # 5 errors[0].source # myfile.py errors[0].level # 2 errors[0].type # WARNING errors[0].message # Title underline too short. errors[0].full_message # Title underline too short. # # Hello World # ======= Contributing ------------ In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Test via ``nosetests``. Donating -------- Support this project and `others by twolfson`_ via `donations`_. http://twolfson.com/support-me .. _`others by twolfson`: http://twolfson.com/projects .. _donations: http://twolfson.com/support-me Unlicense --------- As of Nov 22 2013, Todd Wolfson has released this repository and its contents to the public domain. It has been released under the `UNLICENSE`_. .. _UNLICENSE: https://github.com/twolfson/restructuredtext-lint/blob/master/UNLICENSE UNLICENSE000066400000000000000000000022731344552736200123450ustar00rootroot00000000000000This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. 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 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. For more information, please refer to docs/000077500000000000000000000000001344552736200120215ustar00rootroot00000000000000docs/cli.rst000066400000000000000000000000241344552736200133160ustar00rootroot00000000000000Hello World ======= docs/examples.py000066400000000000000000000006571344552736200142210ustar00rootroot00000000000000import restructuredtext_lint rst = """ Some content. Hello World ======= Some more content! """ errors = restructuredtext_lint.lint(rst, 'myfile.py') print 'errors[0].line #', errors[0].line print 'errors[0].source #', errors[0].source print 'errors[0].level #', errors[0].level print 'errors[0].type #', errors[0].type print 'errors[0].message #', errors[0].message print 'errors[0].full_message #', errors[0].full_message docs/getting_started.py000066400000000000000000000002031344552736200155550ustar00rootroot00000000000000import restructuredtext_lint errors = restructuredtext_lint.lint(""" Hello World ======= """) print errors print errors[0].message docs/sphinx/000077500000000000000000000000001344552736200133325ustar00rootroot00000000000000docs/sphinx/README.rst000066400000000000000000000000731344552736200150210ustar00rootroot00000000000000Hello ===== World .. highlight:: python Hello World! docs/sphinx/index.py000066400000000000000000000006171344552736200150170ustar00rootroot00000000000000# Load in our dependencies from docutils.parsers.rst.directives import register_directive from sphinx.directives.code import Highlight import restructuredtext_lint # Load our new directive register_directive('highlight', Highlight) # Lint our README errors = restructuredtext_lint.lint_file('docs/sphinx/README.rst') print errors[0].message # Error in "highlight" directive: no content permitted. release.sh000077500000000000000000000004041344552736200130460ustar00rootroot00000000000000#!/usr/bin/env bash # Exit on first error set -e # Install our dependencies npm install foundry@~4.3.2 foundry-release-git@~2.0.2 foundry-release-pypi@~3.0.0 # Run foundry release with an adjusted PATH PATH="$PATH:$PWD/node_modules/.bin/" foundry release $* requirements-dev.txt000066400000000000000000000000571344552736200151330ustar00rootroot00000000000000nose==1.3.0 flake8==2.2.3 flake8-quotes==0.0.1 requirements.txt000066400000000000000000000000241344552736200143510ustar00rootroot00000000000000docutils>=0.11,<1.0 restructuredtext_lint/000077500000000000000000000000001344552736200155575ustar00rootroot00000000000000restructuredtext_lint/VERSION000066400000000000000000000000061344552736200166230ustar00rootroot000000000000001.3.0 restructuredtext_lint/__init__.py000066400000000000000000000002641344552736200176720ustar00rootroot00000000000000# Load in our dependencies from __future__ import absolute_import from restructuredtext_lint.lint import lint, lint_file # Export lint functions lint = lint lint_file = lint_file restructuredtext_lint/cli.py000066400000000000000000000100321344552736200166740ustar00rootroot00000000000000# Load in our dependencies from __future__ import absolute_import import argparse from collections import OrderedDict import json import os import sys from docutils.utils import Reporter from restructuredtext_lint.lint import lint_file # Generate our levels mapping constant # DEV: We use an ordered dict for ordering in `--help` # http://repo.or.cz/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/utils/__init__.py#l65 WARNING_LEVEL_KEY = 'warning' LEVEL_MAP = OrderedDict([ ('debug', Reporter.DEBUG_LEVEL), # 0 ('info', Reporter.INFO_LEVEL), # 1 (WARNING_LEVEL_KEY, Reporter.WARNING_LEVEL), # 2 ('error', Reporter.ERROR_LEVEL), # 3 ('severe', Reporter.SEVERE_LEVEL), # 4 ]) # Load in VERSION from standalone file with open(os.path.join(os.path.dirname(__file__), 'VERSION'), 'r') as version_file: VERSION = version_file.read().strip() # Define default contents DEFAULT_FORMAT = 'text' DEFAULT_LEVEL_KEY = WARNING_LEVEL_KEY # Define our CLI function def _main(paths, format=DEFAULT_FORMAT, stream=sys.stdout, encoding=None, level=LEVEL_MAP[DEFAULT_LEVEL_KEY], **kwargs): error_dicts = [] error_occurred = False filepaths = [] for path in paths: # Check if the given path is a file or a directory if os.path.isfile(path): filepaths.append(path) else: # Recurse over subdirectories to search for *.rst files for root, subdir, files in os.walk(path): for file in files: if file.endswith('.rst'): filepaths.append(os.path.join(root, file)) for filepath in filepaths: # Read and lint the file unfiltered_file_errors = lint_file(filepath, encoding=encoding, **kwargs) file_errors = [err for err in unfiltered_file_errors if err.level >= level] if file_errors: error_occurred = True if format == 'text': for err in file_errors: # e.g. WARNING readme.rst:12 Title underline too short. stream.write('{err.type} {err.source}:{err.line} {err.message}\n'.format(err=err)) elif format == 'json': error_dicts.extend({ 'line': error.line, 'source': error.source, 'level': error.level, 'type': error.type, 'message': error.message, 'full_message': error.full_message, } for error in file_errors) if format == 'json': stream.write(json.dumps(error_dicts)) if error_occurred: sys.exit(2) # Using 2 for linting failure, 1 for internal error else: sys.exit(0) # Success! def main(): # Set up options and parse arguments parser = argparse.ArgumentParser(description='Lint reStructuredText files. Returns 0 if all files pass linting, ' '1 for an internal error, and 2 if linting failed.') parser.add_argument('--version', action='version', version=VERSION) parser.add_argument('paths', metavar='path', nargs='+', type=str, help='File/folder to lint') parser.add_argument('--format', default=DEFAULT_FORMAT, type=str, choices=('text', 'json'), help='Format of the output (default: "{default}")'.format(default=DEFAULT_FORMAT)) parser.add_argument('--encoding', type=str, help='Encoding of the input file (e.g. "utf-8")') parser.add_argument('--level', default=DEFAULT_LEVEL_KEY, type=str, choices=LEVEL_MAP.keys(), help='Minimum error level to report (default: "{default}")'.format(default=DEFAULT_LEVEL_KEY)) parser.add_argument('--rst-prolog', type=str, help='reStructuredText content to prepend to all files (useful for substitutions)') args = parser.parse_args() # Convert our level from string to number for `_main` args.level = LEVEL_MAP[args.level] # Run the main argument _main(**args.__dict__) if __name__ == '__main__': main() restructuredtext_lint/lint.py000066400000000000000000000113231344552736200170770ustar00rootroot00000000000000# Load in our dependencies from __future__ import absolute_import import io from docutils import utils from docutils.core import Publisher from docutils.nodes import Element def lint(content, filepath=None, rst_prolog=None): """Lint reStructuredText and return errors :param string content: reStructuredText to be linted :param string filepath: Optional path to file, this will be returned as the source :param string rst_prolog: Optional content to prepend to content, line numbers will be offset to ignore this :rtype list: List of errors. Each error will contain a line, source (filepath), message (error message), and full message (error message + source lines) """ # Generate a new parser (copying `rst2html.py` flow) # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/tools/rst2html.py # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/core.py#l348 pub = Publisher(None, None, None, settings=None) pub.set_components('standalone', 'restructuredtext', 'pseudoxml') # Configure publisher # DEV: We cannot use `process_command_line` since it processes `sys.argv` which is for `rst-lint`, not `docutils` # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/core.py#l201 # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/core.py#l143 # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/core.py#l118 settings = pub.get_settings(halt_level=5) pub.set_io() # Prepare a document to parse on # DEV: We avoid the `read` method because when `source` is `None`, it attempts to read from `stdin`. # However, we already know our content. # DEV: We create our document without `parse` because we need to attach observer's before parsing # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/readers/__init__.py#l66 reader = pub.reader document = utils.new_document(filepath, settings) # Disable stdout # TODO: Find a more proper way to do this # TODO: We might exit the program if a certain error level is reached document.reporter.stream = None # Collect errors via an observer errors = [] # If we have an RST prolog, then prepend it and calculate its offset rst_prolog_line_offset = 0 if rst_prolog: content = rst_prolog + '\n' + content rst_prolog_line_offset = rst_prolog.count('\n') + 1 def error_collector(data): # Mutate the data since it was just generated # DEV: We will generate negative line numbers for RST prolog errors data.line = data.get('line') if isinstance(data.line, int): data.line -= rst_prolog_line_offset data.source = data['source'] data.level = data['level'] data.type = data['type'] data.message = Element.astext(data.children[0]) data.full_message = Element.astext(data) # Save the error errors.append(data) document.reporter.attach_observer(error_collector) # Parse the content (and collect errors) # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/readers/__init__.py#l75 reader.parser.parse(content, document) # Apply transforms (and more collect errors) # DEV: We cannot use `apply_transforms` since it has `attach_observer` baked in. We want only our listener. # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/core.py#l195 # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/transforms/__init__.py#l159 document.transformer.populate_from_components( (pub.source, pub.reader, pub.reader.parser, pub.writer, pub.destination) ) transformer = document.transformer while transformer.transforms: if not transformer.sorted: # Unsorted initially, and whenever a transform is added. transformer.transforms.sort() transformer.transforms.reverse() transformer.sorted = 1 priority, transform_class, pending, kwargs = transformer.transforms.pop() transform = transform_class(transformer.document, startnode=pending) transform.apply(**kwargs) transformer.applied.append((priority, transform_class, pending, kwargs)) return errors def lint_file(filepath, encoding=None, *args, **kwargs): """Lint a specific file""" with io.open(filepath, encoding=encoding) as f: content = f.read() return lint(content, filepath, *args, **kwargs) restructuredtext_lint/test/000077500000000000000000000000001344552736200165365ustar00rootroot00000000000000restructuredtext_lint/test/__init__.py000066400000000000000000000000001344552736200206350ustar00rootroot00000000000000restructuredtext_lint/test/test.py000066400000000000000000000225461344552736200201000ustar00rootroot00000000000000# Load in our dependencies from __future__ import absolute_import import os import subprocess import sys import textwrap from unittest import TestCase import restructuredtext_lint _dir = os.path.dirname(os.path.abspath(__file__)) valid_rst = os.path.join(_dir, 'test_files', 'valid.rst') warning_rst = os.path.join(_dir, 'test_files', 'second_short_heading.rst') dir_rst = os.path.join(_dir, 'test_files', 'dir') invalid_rst = os.path.join(_dir, 'test_files', 'invalid.rst') rst_lint_path = os.path.join(_dir, os.pardir, 'cli.py') """ # TODO: Implement this as a class (options) with a sugar function that lints a string against a set of options An invalid rst file when linted with the `fail_first` parameter raises on the first error """ class TestRestructuredtextLint(TestCase): def _load_file(self, filepath): """Load a file into memory""" f = open(filepath) file = f.read() f.close() return file def _lint_file(self, *args, **kwargs): """Lint the file and preserve any errors""" return restructuredtext_lint.lint(*args, **kwargs) def test_passes_valid_rst(self): """A valid reStructuredText file will not raise any errors""" content = self._load_file(valid_rst) errors = self._lint_file(content) self.assertEqual(errors, []) def test_raises_on_invalid_rst(self): """An invalid reStructuredText file when linted raises errors""" # Load and lint invalid file content = self._load_file(invalid_rst) actual_errors = self._lint_file(content, invalid_rst) # Assert errors against expected errors self.assertEqual(len(actual_errors), 1) self.assertEqual(actual_errors[0].line, 2) self.assertEqual(actual_errors[0].level, 2) self.assertEqual(actual_errors[0].type, 'WARNING') self.assertEqual(actual_errors[0].source, invalid_rst) self.assertEqual(actual_errors[0].message, 'Title underline too short.') def test_encoding_utf8(self): """A document with utf-8 characters is valid.""" filepath = os.path.join(_dir, 'test_files', 'utf8.rst') errors = restructuredtext_lint.lint_file(filepath, encoding='utf-8') self.assertEqual(errors, []) def test_second_heading_short_line_number(self): """A document with a short second heading raises errors that include a line number This is a regression test for https://github.com/twolfson/restructuredtext-lint/issues/5 """ filepath = os.path.join(_dir, 'test_files', 'second_short_heading.rst') errors = restructuredtext_lint.lint_file(filepath) self.assertEqual(errors[0].line, 6) self.assertEqual(errors[0].source, filepath) def test_invalid_target(self): """A document with an invalid target name raises an error This is a regression test for https://github.com/twolfson/restructuredtext-lint/issues/6 """ filepath = os.path.join(_dir, 'test_files', 'invalid_target.rst') errors = restructuredtext_lint.lint_file(filepath) self.assertIn('Unknown target name', errors[0].message) def test_invalid_line_mismatch(self): """A document with an overline/underline mismatch raises an error This is a regression test for https://github.com/twolfson/restructuredtext-lint/issues/7 """ filepath = os.path.join(_dir, 'test_files', 'invalid_line_mismatch.rst') errors = restructuredtext_lint.lint_file(filepath) self.assertIn('Title overline & underline mismatch', errors[0].message) def test_invalid_link(self): """A document with a bad link raises an error This is a regression test for https://github.com/twolfson/restructuredtext-lint/issues/12 """ filepath = os.path.join(_dir, 'test_files', 'invalid_link.rst') errors = restructuredtext_lint.lint_file(filepath) self.assertIn('Anonymous hyperlink mismatch: 1 references but 0 targets.', errors[0].message) self.assertIn('Hyperlink target "hello" is not referenced.', errors[1].message) def test_rst_prolog_basic(self): """A document using substitutions from an `rst-prolog` has no errors""" # https://github.com/twolfson/restructuredtext-lint/issues/39 # Set up our common content rst_prolog = textwrap.dedent(""" .. |World| replace:: Moon """) content = textwrap.dedent(""" Hello ===== |World| """) # Verify we have errors about substitutions without our `--rst-prolog` errors = restructuredtext_lint.lint(content) self.assertEqual(len(errors), 1) self.assertIn('Undefined substitution referenced: "World"', errors[0].message) # Verify we have no errors with our `--rst-prolog` errors = restructuredtext_lint.lint(content, rst_prolog=rst_prolog) self.assertEqual(len(errors), 0) def test_rst_prolog_line_offset(self): """A document with errors using an `rst-prolog` offsets our error lines""" # https://github.com/twolfson/restructuredtext-lint/issues/39 # Perform our setup rst_prolog = textwrap.dedent(""" .. |World| replace:: Moon """) content = textwrap.dedent(""" Hello == |World| """) # Lint our content and assert its errors errors = restructuredtext_lint.lint(content, rst_prolog=rst_prolog) self.assertEqual(len(errors), 1) self.assertIn('Possible title underline, too short for the title', errors[0].message) # DEV: Without adjustments, this would be 6 due to empty lines in multiline strings self.assertEqual(errors[0].line, 3) class TestRestructuredtextLintCLI(TestCase): """ Tests for 'rst-lint' CLI command """ def test_rst_lint_filepaths_not_given(self): """The `rst-lint` command is available and prints error if no filepath was given.""" with self.assertRaises(subprocess.CalledProcessError) as e: # python ../cli.py subprocess.check_output((sys.executable, rst_lint_path), stderr=subprocess.STDOUT) output = str(e.exception.output) # Python 2: "too few arguments" # Python 3: "the following arguments are required: filepath" self.assertIn('arguments', output) def test_rst_lint_correct_file(self): """The `rst-lint` command prints nothing if rst file is correct.""" # python ../cli.py test_files/valid.rst raw_output = subprocess.check_output((sys.executable, rst_lint_path, valid_rst), universal_newlines=True) output = str(raw_output) self.assertEqual(output, '') def test_rst_lint_folder(self): """The `rst-lint` command should print errors for files inside folders.""" with self.assertRaises(subprocess.CalledProcessError) as e: subprocess.check_output((sys.executable, rst_lint_path, dir_rst), universal_newlines=True) output = str(e.exception.output) # Verify exactly 1 error is produced self.assertEqual(output.count('WARNING'), 1) def test_rst_lint_many_files(self): """The `rst-lint` command accepts many rst file paths and prints respective information for each of them.""" with self.assertRaises(subprocess.CalledProcessError) as e: # python ../cli.py test_files/valid.rst invalid.rst subprocess.check_output((sys.executable, rst_lint_path, valid_rst, invalid_rst), universal_newlines=True) output = str(e.exception.output) # 'rst-lint' should exit with error code 2 as linting failed: self.assertEqual(e.exception.returncode, 2) # There should be no clean output: # DEV: This verifies only 1 line of output which is our invalid line self.assertEqual(output.count('\n'), 1, output) # There should be a least one invalid rst file: self.assertIn('WARNING', output) def test_level_fail(self): """Confirm low --level threshold fails file with warnings only""" # This is the expected behaviour we are checking: # $ rst-lint --level warning second_short_heading.rst ; echo "Return code $?" # WARNING second_short_heading.rst:6 Title underline too short. # WARNING second_short_heading.rst:6 Title underline too short. # Return code 2 with self.assertRaises(subprocess.CalledProcessError) as e: subprocess.check_output((sys.executable, rst_lint_path, '--level', 'warning', warning_rst), universal_newlines=True) output = str(e.exception.output) self.assertEqual(output.count('\n'), 2, output) self.assertEqual(output.count('WARNING'), 2, output) # The expected 2 warnings should be treated as failing self.assertEqual(e.exception.returncode, 2) def test_level_high(self): """Confirm high --level threshold accepts file with warnings only""" # This is the expected behaviour we are checking: # $ rst-lint --level error second_short_heading.rst ; echo "Return code $?" # Return code 0 raw_output = subprocess.check_output((sys.executable, rst_lint_path, '--level', 'error', warning_rst), universal_newlines=True) # `check_output` doesn't raise an exception code so it's error code 0 output = str(raw_output) self.assertEqual(output, '') restructuredtext_lint/test/test_files/000077500000000000000000000000001344552736200206775ustar00rootroot00000000000000restructuredtext_lint/test/test_files/dir/000077500000000000000000000000001344552736200214555ustar00rootroot00000000000000restructuredtext_lint/test/test_files/dir/subdir/000077500000000000000000000000001344552736200227455ustar00rootroot00000000000000restructuredtext_lint/test/test_files/dir/subdir/invalid.rst000066400000000000000000000000211344552736200251160ustar00rootroot00000000000000Hello ==== World restructuredtext_lint/test/test_files/dir/valid.rst000066400000000000000000000000221344552736200233000ustar00rootroot00000000000000Hello ===== World restructuredtext_lint/test/test_files/invalid.rst000066400000000000000000000000211344552736200230500ustar00rootroot00000000000000Hello ==== World restructuredtext_lint/test/test_files/invalid_line_mismatch.rst000066400000000000000000000000151344552736200257470ustar00rootroot00000000000000===== b ==== restructuredtext_lint/test/test_files/invalid_link.rst000066400000000000000000000000571344552736200240760ustar00rootroot00000000000000`hello`__ world .. _hello: http://github.com/ restructuredtext_lint/test/test_files/invalid_target.rst000066400000000000000000000001041344552736200244200ustar00rootroot00000000000000`Fork me on GitHub