├── doc8 ├── __init__.py ├── tests │ ├── __init__.py │ └── test_checks.py ├── __main__.py ├── version.py ├── utils.py ├── parser.py ├── checks.py └── main.py ├── doc ├── source │ ├── readme.rst │ ├── usage.rst │ ├── contributing.rst │ ├── installation.rst │ ├── index.rst │ └── conf.py └── requirements.txt ├── test-requirements.txt ├── .gitreview ├── .pre-commit-hooks.yaml ├── CONTRIBUTING.rst ├── requirements.txt ├── tox.ini ├── .pre-commit-config.yaml ├── .travis.yml ├── .gitignore ├── setup.py ├── setup.cfg ├── README.rst └── LICENSE /doc8/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc8/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/source/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../README.rst 2 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | nose # LGPL 2 | testtools # MIT 3 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | pbr # Apache 2 | sphinx>=1.8.0 # BSD 3 | sphinx_rtd_theme>=0.4.0 # MIT 4 | -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=review.openstack.org 3 | port=29418 4 | project=openstack/doc8.git 5 | -------------------------------------------------------------------------------- /doc/source/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | To use doc8 in a project:: 6 | 7 | import doc8 8 | -------------------------------------------------------------------------------- /doc/source/contributing.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | .. include:: ../../CONTRIBUTING.rst 5 | -------------------------------------------------------------------------------- /doc/source/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | $ pip install doc8 8 | 9 | Or, if you have virtualenvwrapper installed:: 10 | 11 | $ mkvirtualenv doc8 12 | $ pip install doc8 13 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # For use with pre-commit. 4 | # See usage instructions at http://pre-commit.com 5 | 6 | - id: doc8 7 | name: doc8 8 | description: This hook runs doc8 for linting docs 9 | entry: python -m doc8 10 | language: python 11 | files: \.rst$ 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Before contributing to *doc8* or any other PyCQA project, we suggest you read 2 | the PyCQA meta documentation: 3 | 4 | http://meta.pycqa.org/en/latest/ 5 | 6 | Patches for *doc8* should be submitted to GitHub, as should bugs: 7 | 8 | https://github.com/pycqa/doc8 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # The order of packages is significant, because pip processes them in the order 2 | # of appearance. Changing the order has an impact on the overall integration 3 | # process, which may cause wedges in the gate later. 4 | 5 | chardet 6 | docutils 7 | restructuredtext-lint>=0.7 8 | six 9 | stevedore 10 | Pygments 11 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to doc8's documentation! 2 | ================================ 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | readme 10 | installation 11 | usage 12 | contributing 13 | 14 | Indices and tables 15 | ================== 16 | 17 | * :ref:`genindex` 18 | * :ref:`modindex` 19 | * :ref:`search` 20 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 1.6 3 | envlist = lint,py{27,35,36,37},docs 4 | 5 | [testenv] 6 | deps = 7 | -r{toxinidir}/test-requirements.txt 8 | commands = nosetests {posargs} 9 | 10 | [testenv:lint] 11 | deps = 12 | pre-commit 13 | commands = 14 | pre-commit run -a 15 | 16 | [testenv:docs] 17 | deps = 18 | -r{toxinidir}/doc/requirements.txt 19 | commands = 20 | doc8 -e .rst doc CONTRIBUTING.rst README.rst 21 | sphinx-build -W -b html doc/source doc/build/html 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks.git 4 | rev: v2.3.0 # Use the ref you want to point at 5 | hooks: 6 | - id: trailing-whitespace 7 | - id: check-yaml 8 | - id: end-of-file-fixer 9 | - id: flake8 10 | - id: trailing-whitespace 11 | - id: check-executables-have-shebangs 12 | - repo: https://github.com/python/black 13 | rev: 19.3b0 14 | hooks: 15 | - id: black 16 | - repo: https://gitlab.com/pycqa/flake8.git 17 | rev: 3.7.8 18 | hooks: 19 | - id: flake8 20 | -------------------------------------------------------------------------------- /doc8/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import sys 16 | 17 | from doc8 import main 18 | 19 | 20 | sys.exit(main.main()) 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dist: xenial 3 | language: python 4 | cache: 5 | - pip 6 | - directories: 7 | - $HOME/.cache 8 | os: 9 | - linux 10 | # tox 3.8.0 is the first version that can boostrap itself 11 | before_script: 12 | - pip install tox>=3.8.0 13 | 14 | # test script 15 | script: tox 16 | notifications: 17 | on_success: change 18 | on_failure: always 19 | 20 | jobs: 21 | fast_finish: true 22 | include: 23 | - name: lint,docs,py37 24 | env: TOXENV=lint,docs 25 | python: "3.7" 26 | - name: py36 27 | python: "3.6" 28 | env: TOXENV=py36 29 | - name: py35 30 | python: "3.5" 31 | env: TOXENV=py35 32 | - name: py27 33 | python: "2.7" 34 | env: TOXENV=py27 35 | 36 | env: 37 | global: 38 | - PIP_DISABLE_PIP_VERSION_CHECK=1 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | doc/build/ 54 | 55 | # pbr stuff 56 | AUTHORS 57 | ChangeLog 58 | -------------------------------------------------------------------------------- /doc8/version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2014 Yahoo! Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | try: 18 | from pbr import version as pbr_version 19 | 20 | _version_info = pbr_version.VersionInfo("doc8") 21 | version_string = _version_info.version_string() 22 | except ImportError: 23 | import pkg_resources 24 | 25 | _version_info = pkg_resources.get_distribution("doc8") 26 | version_string = _version_info.version 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT 17 | import setuptools 18 | 19 | # In python < 2.7.4, a lazy loading of package `pbr` will break 20 | # setuptools if some other modules registered functions in `atexit`. 21 | # solution from: http://bugs.python.org/issue15881#msg170215 22 | try: 23 | import multiprocessing # noqa 24 | except ImportError: 25 | pass 26 | 27 | setuptools.setup(setup_requires=["pbr"], pbr=True) 28 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = doc8 3 | summary = Style checker for Sphinx (or other) RST documentation 4 | description-file = 5 | README.rst 6 | author = OpenStack 7 | author_email = openstack-discuss@lists.openstack.org 8 | maintainer = PyCQA 9 | maintainer_email = code-quality@python.org 10 | home-page = https://github.com/pycqa/doc8 11 | classifier = 12 | Intended Audience :: Information Technology 13 | Intended Audience :: System Administrators 14 | Intended Audience :: Developers 15 | Development Status :: 4 - Beta 16 | Topic :: Utilities 17 | License :: OSI Approved :: Apache Software License 18 | Operating System :: POSIX :: Linux 19 | Programming Language :: Python 20 | Programming Language :: Python :: 2 21 | Programming Language :: Python :: 2.7 22 | Programming Language :: Python :: 3 23 | Programming Language :: Python :: 3.5 24 | Programming Language :: Python :: 3.6 25 | Programming Language :: Python :: 3.7 26 | 27 | [entry_points] 28 | console_scripts = 29 | doc8 = doc8.main:main 30 | 31 | [wheel] 32 | universal = 1 33 | 34 | [flake8] 35 | builtins = _ 36 | show-source = True 37 | exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build 38 | # Minimal config needed to make flake8 compatible with black output: 39 | max-line-length=160 40 | # See https://github.com/PyCQA/pycodestyle/issues/373 41 | extend-ignore = E203 42 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10 | # implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | # -- General configuration ---------------------------------------------------- 15 | 16 | # Add any Sphinx extension module names here, as strings. They can be 17 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 18 | extensions = ["sphinx.ext.autodoc"] 19 | 20 | # The master toctree document. 21 | master_doc = "index" 22 | 23 | # General information about the project. 24 | project = u"doc8" 25 | copyright = u"2013, OpenStack Foundation" 26 | 27 | # The name of the Pygments (syntax highlighting) style to use. 28 | pygments_style = "sphinx" 29 | 30 | # -- Options for HTML output -------------------------------------------------- 31 | 32 | # The theme to use for HTML and HTML Help pages. Major themes that come with 33 | # Sphinx are currently 'default' and 'sphinxdoc'. 34 | html_theme = "sphinx_rtd_theme" 35 | -------------------------------------------------------------------------------- /doc8/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Ivan Melnikov 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import glob 16 | import os 17 | 18 | 19 | def find_files(paths, extensions, ignored_paths): 20 | extensions = set(extensions) 21 | ignored_absolute_paths = set() 22 | for path in ignored_paths: 23 | for expanded_path in glob.iglob(path): 24 | expanded_path = os.path.abspath(expanded_path) 25 | ignored_absolute_paths.add(expanded_path) 26 | 27 | def extension_matches(path): 28 | _base, ext = os.path.splitext(path) 29 | return ext in extensions 30 | 31 | def path_ignorable(path): 32 | path = os.path.abspath(path) 33 | if path in ignored_absolute_paths: 34 | return True 35 | last_path = None 36 | while path != last_path: 37 | # If we hit the root, this loop will stop since the resolution 38 | # of "/../" is still "/" when ran through the abspath function... 39 | last_path = path 40 | path = os.path.abspath(os.path.join(path, os.path.pardir)) 41 | if path in ignored_absolute_paths: 42 | return True 43 | return False 44 | 45 | for path in paths: 46 | if os.path.isfile(path): 47 | if extension_matches(path): 48 | yield (path, path_ignorable(path)) 49 | elif os.path.isdir(path): 50 | for root, dirnames, filenames in os.walk(path): 51 | for filename in filenames: 52 | path = os.path.join(root, filename) 53 | if extension_matches(path): 54 | yield (path, path_ignorable(path)) 55 | else: 56 | raise IOError("Invalid path: %s" % path) 57 | 58 | 59 | def filtered_traverse(document, filter_func): 60 | for n in document.traverse(include_self=True): 61 | if filter_func(n): 62 | yield n 63 | 64 | 65 | def contains_url(line): 66 | return "http://" in line or "https://" in line 67 | 68 | 69 | def has_any_node_type(node, node_types): 70 | n = node 71 | while n is not None: 72 | if isinstance(n, node_types): 73 | return True 74 | n = n.parent 75 | return False 76 | -------------------------------------------------------------------------------- /doc8/parser.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Ivan Melnikov 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import errno 16 | import os 17 | import threading 18 | 19 | import chardet 20 | from docutils import frontend 21 | from docutils import parsers as docutils_parser 22 | from docutils import utils 23 | import restructuredtext_lint as rl 24 | import six 25 | 26 | 27 | class ParsedFile(object): 28 | FALLBACK_ENCODING = "utf-8" 29 | 30 | def __init__(self, filename, encoding=None, default_extension=""): 31 | self._filename = filename 32 | self._content = None 33 | self._raw_content = None 34 | self._encoding = encoding 35 | self._doc = None 36 | self._errors = None 37 | self._lines = None 38 | self._has_read = False 39 | self._extension = os.path.splitext(filename)[1] 40 | self._read_lock = threading.Lock() 41 | if not self._extension: 42 | self._extension = default_extension 43 | 44 | @property 45 | def errors(self): 46 | if self._errors is not None: 47 | return self._errors 48 | self._errors = rl.lint(self.contents, filepath=self.filename) 49 | return self._errors 50 | 51 | @property 52 | def document(self): 53 | if self._doc is None: 54 | # Use the rst parsers document output to do as much of the 55 | # validation as we can without resorting to custom logic (this 56 | # parser is what sphinx and others use anyway so it's hopefully 57 | # mature). 58 | parser_cls = docutils_parser.get_parser_class("rst") 59 | parser = parser_cls() 60 | defaults = { 61 | "halt_level": 5, 62 | "report_level": 5, 63 | "quiet": True, 64 | "file_insertion_enabled": False, 65 | "traceback": True, 66 | # Development use only. 67 | "dump_settings": False, 68 | "dump_internals": False, 69 | "dump_transforms": False, 70 | } 71 | opt = frontend.OptionParser(components=[parser], defaults=defaults) 72 | doc = utils.new_document( 73 | source_path=self.filename, settings=opt.get_default_values() 74 | ) 75 | parser.parse(self.contents, doc) 76 | self._doc = doc 77 | return self._doc 78 | 79 | def _read(self): 80 | if self._has_read: 81 | return 82 | with self._read_lock: 83 | if not self._has_read: 84 | with open(self.filename, "rb") as fh: 85 | self._lines = list(fh) 86 | fh.seek(0) 87 | self._raw_content = fh.read() 88 | self._has_read = True 89 | 90 | def lines_iter(self, remove_trailing_newline=True): 91 | self._read() 92 | for line in self._lines: 93 | line = six.text_type(line, encoding=self.encoding) 94 | if remove_trailing_newline and line.endswith("\n"): 95 | line = line[0:-1] 96 | yield line 97 | 98 | @property 99 | def lines(self): 100 | self._read() 101 | return self._lines 102 | 103 | @property 104 | def extension(self): 105 | return self._extension 106 | 107 | @property 108 | def filename(self): 109 | return self._filename 110 | 111 | @property 112 | def encoding(self): 113 | if not self._encoding: 114 | encoding = chardet.detect(self.raw_contents)["encoding"] 115 | if not encoding: 116 | encoding = self.FALLBACK_ENCODING 117 | self._encoding = encoding 118 | return self._encoding 119 | 120 | @property 121 | def raw_contents(self): 122 | self._read() 123 | return self._raw_content 124 | 125 | @property 126 | def contents(self): 127 | if self._content is None: 128 | self._content = six.text_type(self.raw_contents, encoding=self.encoding) 129 | return self._content 130 | 131 | def __str__(self): 132 | return "%s (%s, %s chars, %s lines)" % ( 133 | self.filename, 134 | self.encoding, 135 | len(self.contents), 136 | len(list(self.lines_iter())), 137 | ) 138 | 139 | 140 | def parse(filename, encoding=None, default_extension=""): 141 | if not os.path.isfile(filename): 142 | raise IOError(errno.ENOENT, "File not found", filename) 143 | return ParsedFile(filename, encoding=encoding, default_extension=default_extension) 144 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.com/PyCQA/doc8.svg?branch=master 2 | :target: https://travis-ci.com/PyCQA/doc8 3 | .. image:: https://img.shields.io/pypi/v/doc8 4 | :alt: PyPI 5 | :target: https://pypi.org/project/doc8/ 6 | .. image:: https://img.shields.io/pypi/l/doc8 7 | :alt: PyPI - License 8 | .. image:: https://img.shields.io/github/last-commit/pycqa/doc8 9 | :alt: GitHub last commit 10 | 11 | ==== 12 | Doc8 13 | ==== 14 | 15 | Doc8 is an *opinionated* style checker for `rst`_ (with basic support for 16 | plain text) styles of documentation. 17 | 18 | QuickStart 19 | ========== 20 | 21 | :: 22 | 23 | pip install doc8 24 | 25 | To run doc8 just invoke it against any doc directory:: 26 | 27 | $ doc8 coolproject/docs 28 | 29 | Usage 30 | ===== 31 | 32 | Command line usage 33 | ****************** 34 | 35 | :: 36 | 37 | $ doc8 -h 38 | 39 | usage: doc8 [-h] [--config path] [--allow-long-titles] [--ignore code] 40 | [--no-sphinx] [--ignore-path path] [--ignore-path-errors path] 41 | [--default-extension extension] [--file-encoding encoding] 42 | [--max-line-length int] [-e extension] [-v] [--version] 43 | [path [path ...]] 44 | 45 | Check documentation for simple style requirements. 46 | 47 | What is checked: 48 | - invalid rst format - D000 49 | - lines should not be longer than 79 characters - D001 50 | - RST exception: line with no whitespace except in the beginning 51 | - RST exception: lines with http or https urls 52 | - RST exception: literal blocks 53 | - RST exception: rst target directives 54 | - no trailing whitespace - D002 55 | - no tabulation for indentation - D003 56 | - no carriage returns (use unix newlines) - D004 57 | - no newline at end of file - D005 58 | 59 | positional arguments: 60 | path Path to scan for doc files (default: current 61 | directory). 62 | 63 | optional arguments: 64 | -h, --help show this help message and exit 65 | --config path user config file location (default: doc8.ini, tox.ini, 66 | pep8.ini, setup.cfg). 67 | --allow-long-titles allow long section titles (default: false). 68 | --ignore code ignore the given error code(s). 69 | --no-sphinx do not ignore sphinx specific false positives. 70 | --ignore-path path ignore the given directory or file (globs are 71 | supported). 72 | --ignore-path-errors path 73 | ignore the given specific errors in the provided file. 74 | --default-extension extension 75 | default file extension to use when a file is found 76 | without a file extension. 77 | --file-encoding encoding 78 | override encoding to use when attempting to determine 79 | an input files text encoding (providing this avoids 80 | using `chardet` to automatically detect encoding/s) 81 | --max-line-length int 82 | maximum allowed line length (default: 79). 83 | -e extension, --extension extension 84 | check file extensions of the given type (default: 85 | .rst, .txt). 86 | -q, --quiet only print violations 87 | -v, --verbose run in verbose mode. 88 | --version show the version and exit. 89 | 90 | Ini file usage 91 | ************** 92 | 93 | Instead of using the CLI for options the following files will also be examined 94 | for ``[doc8]`` sections that can also provided the same set of options. If 95 | the ``--config path`` option is used these files will **not** be scanned for 96 | the current working directory and that configuration path will be used 97 | instead. 98 | 99 | * ``$CWD/doc8.ini`` 100 | * ``$CWD/tox.ini`` 101 | * ``$CWD/pep8.ini`` 102 | * ``$CWD/setup.cfg`` 103 | 104 | An example section that can be placed into one of these files:: 105 | 106 | [doc8] 107 | 108 | ignore-path=/tmp/stuff,/tmp/other_stuff 109 | max-line-length=99 110 | verbose=1 111 | ignore-path-errors=/tmp/other_thing.rst;D001;D002 112 | 113 | **Note:** The option names are the same as the command line ones (with the 114 | only variation of this being the ``no-sphinx`` option which from 115 | configuration file will be ``sphinx`` instead). 116 | 117 | Option conflict resolution 118 | ************************** 119 | 120 | When the same option is passed on the command line and also via configuration 121 | files the following strategies are applied to resolve these types 122 | of conflicts. 123 | 124 | ====================== =========== ======== 125 | Option Overrides Merges 126 | ====================== =========== ======== 127 | ``allow-long-titles`` Yes No 128 | ``ignore-path-errors`` No Yes 129 | ``default-extension`` Yes No 130 | ``extension`` No Yes 131 | ``ignore-path`` No Yes 132 | ``ignore`` No Yes 133 | ``max-line-length`` Yes No 134 | ``file-encoding`` Yes No 135 | ``sphinx`` Yes No 136 | ====================== =========== ======== 137 | 138 | **Note:** In the above table the configuration file option when specified as 139 | *overrides* will replace the same option given via the command line. When 140 | *merges* is stated then the option will be combined with the command line 141 | option (for example by becoming a larger list or set of values that contains 142 | the values passed on the command line *and* the values passed via 143 | configuration). 144 | 145 | .. _rst: http://docutils.sourceforge.net/docs/ref/rst/introduction.html 146 | -------------------------------------------------------------------------------- /doc8/tests/test_checks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2014 Yahoo! Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | import tempfile 18 | 19 | import testtools 20 | 21 | from doc8 import checks 22 | from doc8 import parser 23 | 24 | 25 | class TestTrailingWhitespace(testtools.TestCase): 26 | def test_trailing(self): 27 | lines = ["a b ", "ab"] 28 | check = checks.CheckTrailingWhitespace({}) 29 | errors = [] 30 | for line in lines: 31 | errors.extend(check.report_iter(line)) 32 | self.assertEqual(1, len(errors)) 33 | (code, msg) = errors[0] 34 | self.assertIn(code, check.REPORTS) 35 | 36 | 37 | class TestTabIndentation(testtools.TestCase): 38 | def test_tabs(self): 39 | lines = [" b", "\tabc", "efg", "\t\tc"] 40 | check = checks.CheckIndentationNoTab({}) 41 | errors = [] 42 | for line in lines: 43 | errors.extend(check.report_iter(line)) 44 | self.assertEqual(2, len(errors)) 45 | (code, msg) = errors[0] 46 | self.assertIn(code, check.REPORTS) 47 | 48 | 49 | class TestCarriageReturn(testtools.TestCase): 50 | def test_cr(self): 51 | lines = ["\tabc", "efg", "\r\n"] 52 | check = checks.CheckCarriageReturn({}) 53 | errors = [] 54 | for line in lines: 55 | errors.extend(check.report_iter(line)) 56 | self.assertEqual(1, len(errors)) 57 | (code, msg) = errors[0] 58 | self.assertIn(code, check.REPORTS) 59 | 60 | 61 | class TestLineLength(testtools.TestCase): 62 | def test_over_length(self): 63 | content = b""" 64 | === 65 | aaa 66 | === 67 | 68 | ---- 69 | test 70 | ---- 71 | 72 | """ 73 | content += b"\n\n" 74 | content += (b"a" * 60) + b" " + (b"b" * 60) 75 | content += b"\n" 76 | conf = {"max_line_length": 79, "allow_long_titles": True} 77 | for ext in [".rst", ".txt"]: 78 | with tempfile.NamedTemporaryFile(suffix=ext) as fh: 79 | fh.write(content) 80 | fh.flush() 81 | 82 | parsed_file = parser.ParsedFile(fh.name) 83 | check = checks.CheckMaxLineLength(conf) 84 | errors = list(check.report_iter(parsed_file)) 85 | self.assertEqual(1, len(errors)) 86 | (line, code, msg) = errors[0] 87 | self.assertIn(code, check.REPORTS) 88 | 89 | def test_correct_length(self): 90 | conf = {"max_line_length": 79, "allow_long_titles": True} 91 | with tempfile.NamedTemporaryFile(suffix=".rst") as fh: 92 | fh.write( 93 | b"known exploit in the wild, for example" 94 | b" \xe2\x80\x93 the time" 95 | b" between advance notification" 96 | ) 97 | fh.flush() 98 | 99 | parsed_file = parser.ParsedFile(fh.name, encoding="utf-8") 100 | check = checks.CheckMaxLineLength(conf) 101 | errors = list(check.report_iter(parsed_file)) 102 | self.assertEqual(0, len(errors)) 103 | 104 | def test_ignore_code_block(self): 105 | conf = {"max_line_length": 79, "allow_long_titles": True} 106 | with tempfile.NamedTemporaryFile(suffix=".rst") as fh: 107 | fh.write( 108 | b"List which contains items with code-block\n" 109 | b"- this is a list item\n\n" 110 | b" .. code-block:: ini\n\n" 111 | b" this line exceeds 80 chars but should be ignored" 112 | b"this line exceeds 80 chars but should be ignored" 113 | b"this line exceeds 80 chars but should be ignored" 114 | ) 115 | fh.flush() 116 | 117 | parsed_file = parser.ParsedFile(fh.name, encoding="utf-8") 118 | check = checks.CheckMaxLineLength(conf) 119 | errors = list(check.report_iter(parsed_file)) 120 | self.assertEqual(0, len(errors)) 121 | 122 | def test_unsplittable_length(self): 123 | content = b""" 124 | === 125 | aaa 126 | === 127 | 128 | ---- 129 | test 130 | ---- 131 | 132 | """ 133 | content += b"\n\n" 134 | content += b"a" * 100 135 | content += b"\n" 136 | conf = {"max_line_length": 79, "allow_long_titles": True} 137 | # This number is different since rst parsing is aware that titles 138 | # are allowed to be over-length, while txt parsing is not aware of 139 | # this fact (since it has no concept of title sections). 140 | extensions = [(0, ".rst"), (1, ".txt")] 141 | for expected_errors, ext in extensions: 142 | with tempfile.NamedTemporaryFile(suffix=ext) as fh: 143 | fh.write(content) 144 | fh.flush() 145 | 146 | parsed_file = parser.ParsedFile(fh.name) 147 | check = checks.CheckMaxLineLength(conf) 148 | errors = list(check.report_iter(parsed_file)) 149 | self.assertEqual(expected_errors, len(errors)) 150 | 151 | def test_definition_term_length(self): 152 | conf = {"max_line_length": 79, "allow_long_titles": True} 153 | with tempfile.NamedTemporaryFile(suffix=".rst") as fh: 154 | fh.write( 155 | b"Definition List which contains long term.\n\n" 156 | b"looooooooooooooooooooooooooooooong definition term" 157 | b"this line exceeds 80 chars but should be ignored\n" 158 | b" this is a definition\n" 159 | ) 160 | fh.flush() 161 | 162 | parsed_file = parser.ParsedFile(fh.name, encoding="utf-8") 163 | check = checks.CheckMaxLineLength(conf) 164 | errors = list(check.report_iter(parsed_file)) 165 | self.assertEqual(0, len(errors)) 166 | 167 | 168 | class TestNewlineEndOfFile(testtools.TestCase): 169 | def test_newline(self): 170 | tests = [ 171 | (1, b"testing"), 172 | (1, b"testing\ntesting"), 173 | (0, b"testing\n"), 174 | (0, b"testing\ntesting\n"), 175 | ] 176 | 177 | for expected_errors, line in tests: 178 | with tempfile.NamedTemporaryFile() as fh: 179 | fh.write(line) 180 | fh.flush() 181 | parsed_file = parser.ParsedFile(fh.name) 182 | check = checks.CheckNewlineEndOfFile({}) 183 | errors = list(check.report_iter(parsed_file)) 184 | self.assertEqual(expected_errors, len(errors)) 185 | -------------------------------------------------------------------------------- /doc8/checks.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Ivan Melnikov 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import abc 16 | import collections 17 | import re 18 | 19 | from docutils import nodes as docutils_nodes 20 | import six 21 | 22 | from doc8 import utils 23 | 24 | 25 | @six.add_metaclass(abc.ABCMeta) 26 | class ContentCheck(object): 27 | def __init__(self, cfg): 28 | self._cfg = cfg 29 | 30 | @abc.abstractmethod 31 | def report_iter(self, parsed_file): 32 | pass 33 | 34 | 35 | @six.add_metaclass(abc.ABCMeta) 36 | class LineCheck(object): 37 | def __init__(self, cfg): 38 | self._cfg = cfg 39 | 40 | @abc.abstractmethod 41 | def report_iter(self, line): 42 | pass 43 | 44 | 45 | class CheckTrailingWhitespace(LineCheck): 46 | _TRAILING_WHITESPACE_REGEX = re.compile(r"\s$") 47 | REPORTS = frozenset(["D002"]) 48 | 49 | def report_iter(self, line): 50 | if self._TRAILING_WHITESPACE_REGEX.search(line): 51 | yield ("D002", "Trailing whitespace") 52 | 53 | 54 | class CheckIndentationNoTab(LineCheck): 55 | _STARTING_WHITESPACE_REGEX = re.compile(r"^(\s+)") 56 | REPORTS = frozenset(["D003"]) 57 | 58 | def report_iter(self, line): 59 | match = self._STARTING_WHITESPACE_REGEX.search(line) 60 | if match: 61 | spaces = match.group(1) 62 | if "\t" in spaces: 63 | yield ("D003", "Tabulation used for indentation") 64 | 65 | 66 | class CheckCarriageReturn(LineCheck): 67 | REPORTS = frozenset(["D004"]) 68 | 69 | def report_iter(self, line): 70 | if "\r" in line: 71 | yield ("D004", "Found literal carriage return") 72 | 73 | 74 | class CheckNewlineEndOfFile(ContentCheck): 75 | REPORTS = frozenset(["D005"]) 76 | 77 | def __init__(self, cfg): 78 | super(CheckNewlineEndOfFile, self).__init__(cfg) 79 | 80 | def report_iter(self, parsed_file): 81 | if parsed_file.lines and not parsed_file.lines[-1].endswith(b"\n"): 82 | yield (len(parsed_file.lines), "D005", "No newline at end of file") 83 | 84 | 85 | class CheckValidity(ContentCheck): 86 | REPORTS = frozenset(["D000"]) 87 | EXT_MATCHER = re.compile(r"(.*)[.]rst", re.I) 88 | 89 | # From docutils docs: 90 | # 91 | # Report system messages at or higher than : "info" or "1", 92 | # "warning"/"2" (default), "error"/"3", "severe"/"4", "none"/"5" 93 | # 94 | # See: http://docutils.sourceforge.net/docs/user/config.html#report-level 95 | WARN_LEVELS = frozenset([2, 3, 4]) 96 | 97 | # Only used when running in sphinx mode. 98 | SPHINX_IGNORES_REGEX = [ 99 | re.compile(r"^Unknown interpreted text"), 100 | re.compile(r"^Unknown directive type"), 101 | re.compile(r"^Undefined substitution"), 102 | re.compile(r"^Substitution definition contains illegal element"), 103 | ] 104 | 105 | def __init__(self, cfg): 106 | super(CheckValidity, self).__init__(cfg) 107 | self._sphinx_mode = cfg.get("sphinx") 108 | 109 | def report_iter(self, parsed_file): 110 | for error in parsed_file.errors: 111 | if error.level not in self.WARN_LEVELS: 112 | continue 113 | ignore = False 114 | if self._sphinx_mode: 115 | for m in self.SPHINX_IGNORES_REGEX: 116 | if m.match(error.message): 117 | ignore = True 118 | break 119 | if not ignore: 120 | yield (error.line, "D000", error.message) 121 | 122 | 123 | class CheckMaxLineLength(ContentCheck): 124 | REPORTS = frozenset(["D001"]) 125 | 126 | def __init__(self, cfg): 127 | super(CheckMaxLineLength, self).__init__(cfg) 128 | self._max_line_length = self._cfg["max_line_length"] 129 | self._allow_long_titles = self._cfg["allow_long_titles"] 130 | 131 | def _extract_node_lines(self, doc): 132 | def extract_lines(node, start_line): 133 | lines = [start_line] 134 | if isinstance(node, (docutils_nodes.title)): 135 | start = start_line - len(node.rawsource.splitlines()) 136 | if start >= 0: 137 | lines.append(start) 138 | if isinstance(node, (docutils_nodes.literal_block)): 139 | end = start_line + len(node.rawsource.splitlines()) - 1 140 | lines.append(end) 141 | return lines 142 | 143 | def gather_lines(node): 144 | lines = [] 145 | for n in node.traverse(include_self=True): 146 | lines.extend(extract_lines(n, find_line(n))) 147 | return lines 148 | 149 | def find_line(node): 150 | n = node 151 | while n is not None: 152 | if n.line is not None: 153 | return n.line 154 | n = n.parent 155 | return None 156 | 157 | def filter_systems(node): 158 | if utils.has_any_node_type(node, (docutils_nodes.system_message,)): 159 | return False 160 | return True 161 | 162 | nodes_lines = [] 163 | first_line = -1 164 | for n in utils.filtered_traverse(doc, filter_systems): 165 | line = find_line(n) 166 | if line is None: 167 | continue 168 | if first_line == -1: 169 | first_line = line 170 | contained_lines = set(gather_lines(n)) 171 | nodes_lines.append((n, (min(contained_lines), max(contained_lines)))) 172 | return (nodes_lines, first_line) 173 | 174 | def _extract_directives(self, lines): 175 | def starting_whitespace(line): 176 | m = re.match(r"^(\s+)(.*)$", line) 177 | if not m: 178 | return 0 179 | return len(m.group(1)) 180 | 181 | def all_whitespace(line): 182 | return bool(re.match(r"^(\s*)$", line)) 183 | 184 | def find_directive_end(start, lines): 185 | after_lines = collections.deque(lines[start + 1 :]) 186 | k = 0 187 | while after_lines: 188 | line = after_lines.popleft() 189 | if all_whitespace(line) or starting_whitespace(line) >= 1: 190 | k += 1 191 | else: 192 | break 193 | return start + k 194 | 195 | # Find where directives start & end so that we can exclude content in 196 | # these directive regions (the rst parser may not handle this correctly 197 | # for unknown directives, so we have to do it manually). 198 | directives = [] 199 | for i, line in enumerate(lines): 200 | if re.match(r"^\s*..\s(.*?)::\s*", line): 201 | directives.append((i, find_directive_end(i, lines))) 202 | elif re.match(r"^::\s*$", line): 203 | directives.append((i, find_directive_end(i, lines))) 204 | 205 | # Find definition terms in definition lists 206 | # This check may match the code, which is already appended 207 | lwhitespaces = r"^\s*" 208 | listspattern = r"^\s*(\* |- |#\. |\d+\. )" 209 | for i in range(0, len(lines) - 1): 210 | line = lines[i] 211 | next_line = lines[i + 1] 212 | # if line is a blank, line is not a definition term 213 | if all_whitespace(line): 214 | continue 215 | # if line is a list, line is checked as normal line 216 | if re.match(listspattern, line): 217 | continue 218 | if len(re.search(lwhitespaces, line).group()) < len( 219 | re.search(lwhitespaces, next_line).group() 220 | ): 221 | directives.append((i, i)) 222 | 223 | return directives 224 | 225 | def _txt_checker(self, parsed_file): 226 | for i, line in enumerate(parsed_file.lines_iter()): 227 | if len(line) > self._max_line_length: 228 | if not utils.contains_url(line): 229 | yield (i + 1, "D001", "Line too long") 230 | 231 | def _rst_checker(self, parsed_file): 232 | lines = list(parsed_file.lines_iter()) 233 | doc = parsed_file.document 234 | nodes_lines, first_line = self._extract_node_lines(doc) 235 | directives = self._extract_directives(lines) 236 | 237 | def find_containing_nodes(num): 238 | if num < first_line and len(nodes_lines): 239 | return [nodes_lines[0][0]] 240 | contained_in = [] 241 | for (n, (line_min, line_max)) in nodes_lines: 242 | if num >= line_min and num <= line_max: 243 | contained_in.append((n, (line_min, line_max))) 244 | smallest_span = None 245 | best_nodes = [] 246 | for (n, (line_min, line_max)) in contained_in: 247 | span = line_max - line_min 248 | if smallest_span is None: 249 | smallest_span = span 250 | best_nodes = [n] 251 | elif span < smallest_span: 252 | smallest_span = span 253 | best_nodes = [n] 254 | elif span == smallest_span: 255 | best_nodes.append(n) 256 | return best_nodes 257 | 258 | def any_types(nodes, types): 259 | return any([isinstance(n, types) for n in nodes]) 260 | 261 | skip_types = (docutils_nodes.target, docutils_nodes.literal_block) 262 | title_types = ( 263 | docutils_nodes.title, 264 | docutils_nodes.subtitle, 265 | docutils_nodes.section, 266 | ) 267 | for i, line in enumerate(lines): 268 | if len(line) > self._max_line_length: 269 | in_directive = False 270 | for (start, end) in directives: 271 | if i >= start and i <= end: 272 | in_directive = True 273 | break 274 | if in_directive: 275 | continue 276 | stripped = line.lstrip() 277 | if " " not in stripped: 278 | # No room to split even if we could. 279 | continue 280 | if utils.contains_url(stripped): 281 | continue 282 | nodes = find_containing_nodes(i + 1) 283 | if any_types(nodes, skip_types): 284 | continue 285 | if self._allow_long_titles and any_types(nodes, title_types): 286 | continue 287 | yield (i + 1, "D001", "Line too long") 288 | 289 | def report_iter(self, parsed_file): 290 | if parsed_file.extension.lower() != ".rst": 291 | checker_func = self._txt_checker 292 | else: 293 | checker_func = self._rst_checker 294 | for issue in checker_func(parsed_file): 295 | yield issue 296 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /doc8/main.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Ivan Melnikov 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | 16 | """Check documentation for simple style requirements. 17 | 18 | What is checked: 19 | - invalid rst format - D000 20 | - lines should not be longer than 79 characters - D001 21 | - RST exception: line with no whitespace except in the beginning 22 | - RST exception: lines with http or https urls 23 | - RST exception: literal blocks 24 | - RST exception: rst target directives 25 | - no trailing whitespace - D002 26 | - no tabulation for indentation - D003 27 | - no carriage returns (use unix newlines) - D004 28 | - no newline at end of file - D005 29 | """ 30 | 31 | import argparse 32 | import collections 33 | import logging 34 | import os 35 | import sys 36 | 37 | import six 38 | from six.moves import configparser 39 | from stevedore import extension 40 | 41 | from doc8 import checks 42 | from doc8 import parser as file_parser 43 | from doc8 import utils 44 | from doc8 import version 45 | 46 | FILE_PATTERNS = [".rst", ".txt"] 47 | MAX_LINE_LENGTH = 79 48 | CONFIG_FILENAMES = ["doc8.ini", "tox.ini", "pep8.ini", "setup.cfg"] 49 | 50 | 51 | def split_set_type(text, delimiter=","): 52 | return set([i.strip() for i in text.split(delimiter) if i.strip()]) 53 | 54 | 55 | def merge_sets(sets): 56 | m = set() 57 | for s in sets: 58 | m.update(s) 59 | return m 60 | 61 | 62 | def parse_ignore_path_errors(entries): 63 | ignore_path_errors = collections.defaultdict(set) 64 | for path in entries: 65 | path, ignored_errors = path.split(";", 1) 66 | path = path.strip() 67 | ignored_errors = split_set_type(ignored_errors, delimiter=";") 68 | ignore_path_errors[path].update(ignored_errors) 69 | return dict(ignore_path_errors) 70 | 71 | 72 | def extract_config(args): 73 | parser = configparser.RawConfigParser() 74 | read_files = [] 75 | if args["config"]: 76 | for fn in args["config"]: 77 | with open(fn, "r") as fh: 78 | parser.readfp(fh, filename=fn) 79 | read_files.append(fn) 80 | else: 81 | read_files.extend(parser.read(CONFIG_FILENAMES)) 82 | if not read_files: 83 | return {} 84 | cfg = {} 85 | try: 86 | cfg["max_line_length"] = parser.getint("doc8", "max-line-length") 87 | except (configparser.NoSectionError, configparser.NoOptionError): 88 | pass 89 | try: 90 | cfg["ignore"] = split_set_type(parser.get("doc8", "ignore")) 91 | except (configparser.NoSectionError, configparser.NoOptionError): 92 | pass 93 | try: 94 | cfg["ignore_path"] = split_set_type(parser.get("doc8", "ignore-path")) 95 | except (configparser.NoSectionError, configparser.NoOptionError): 96 | pass 97 | try: 98 | ignore_path_errors = parser.get("doc8", "ignore-path-errors") 99 | ignore_path_errors = split_set_type(ignore_path_errors) 100 | ignore_path_errors = parse_ignore_path_errors(ignore_path_errors) 101 | cfg["ignore_path_errors"] = ignore_path_errors 102 | except (configparser.NoSectionError, configparser.NoOptionError): 103 | pass 104 | try: 105 | cfg["allow_long_titles"] = parser.getboolean("doc8", "allow-long-titles") 106 | except (configparser.NoSectionError, configparser.NoOptionError): 107 | pass 108 | try: 109 | cfg["sphinx"] = parser.getboolean("doc8", "sphinx") 110 | except (configparser.NoSectionError, configparser.NoOptionError): 111 | pass 112 | try: 113 | cfg["verbose"] = parser.getboolean("doc8", "verbose") 114 | except (configparser.NoSectionError, configparser.NoOptionError): 115 | pass 116 | try: 117 | cfg["file_encoding"] = parser.get("doc8", "file-encoding") 118 | except (configparser.NoSectionError, configparser.NoOptionError): 119 | pass 120 | try: 121 | cfg["default_extension"] = parser.get("doc8", "default-extension") 122 | except (configparser.NoSectionError, configparser.NoOptionError): 123 | pass 124 | try: 125 | extensions = parser.get("doc8", "extensions") 126 | extensions = extensions.split(",") 127 | extensions = [s.strip() for s in extensions if s.strip()] 128 | if extensions: 129 | cfg["extension"] = extensions 130 | except (configparser.NoSectionError, configparser.NoOptionError): 131 | pass 132 | return cfg 133 | 134 | 135 | def fetch_checks(cfg): 136 | base = [ 137 | checks.CheckValidity(cfg), 138 | checks.CheckTrailingWhitespace(cfg), 139 | checks.CheckIndentationNoTab(cfg), 140 | checks.CheckCarriageReturn(cfg), 141 | checks.CheckMaxLineLength(cfg), 142 | checks.CheckNewlineEndOfFile(cfg), 143 | ] 144 | mgr = extension.ExtensionManager( 145 | namespace="doc8.extension.check", invoke_on_load=True, invoke_args=(cfg.copy(),) 146 | ) 147 | addons = [] 148 | for e in mgr: 149 | addons.append(e.obj) 150 | return base + addons 151 | 152 | 153 | def setup_logging(verbose): 154 | if verbose: 155 | level = logging.DEBUG 156 | else: 157 | level = logging.ERROR 158 | logging.basicConfig( 159 | level=level, format="%(levelname)s: %(message)s", stream=sys.stdout 160 | ) 161 | 162 | 163 | def scan(cfg): 164 | if not cfg.get("quiet"): 165 | print("Scanning...") 166 | files = collections.deque() 167 | ignored_paths = cfg.get("ignore_path", []) 168 | files_ignored = 0 169 | file_iter = utils.find_files( 170 | cfg.get("paths", []), cfg.get("extension", []), ignored_paths 171 | ) 172 | default_extension = cfg.get("default_extension") 173 | file_encoding = cfg.get("file_encoding") 174 | for filename, ignoreable in file_iter: 175 | if ignoreable: 176 | files_ignored += 1 177 | if cfg.get("verbose"): 178 | print(" Ignoring '%s'" % (filename)) 179 | else: 180 | f = file_parser.parse( 181 | filename, default_extension=default_extension, encoding=file_encoding 182 | ) 183 | files.append(f) 184 | if cfg.get("verbose"): 185 | print(" Selecting '%s'" % (filename)) 186 | return (files, files_ignored) 187 | 188 | 189 | def validate(cfg, files): 190 | if not cfg.get("quiet"): 191 | print("Validating...") 192 | error_counts = {} 193 | ignoreables = frozenset(cfg.get("ignore", [])) 194 | ignore_targeted = cfg.get("ignore_path_errors", {}) 195 | while files: 196 | f = files.popleft() 197 | if cfg.get("verbose"): 198 | print("Validating %s" % f) 199 | targeted_ignoreables = set(ignore_targeted.get(f.filename, set())) 200 | targeted_ignoreables.update(ignoreables) 201 | for c in fetch_checks(cfg): 202 | try: 203 | # http://legacy.python.org/dev/peps/pep-3155/ 204 | check_name = c.__class__.__qualname__ 205 | except AttributeError: 206 | check_name = ".".join([c.__class__.__module__, c.__class__.__name__]) 207 | error_counts.setdefault(check_name, 0) 208 | try: 209 | extension_matcher = c.EXT_MATCHER 210 | except AttributeError: 211 | pass 212 | else: 213 | if not extension_matcher.match(f.extension): 214 | if cfg.get("verbose"): 215 | print( 216 | " Skipping check '%s' since it does not" 217 | " understand parsing a file with extension '%s'" 218 | % (check_name, f.extension) 219 | ) 220 | continue 221 | try: 222 | reports = set(c.REPORTS) 223 | except AttributeError: 224 | pass 225 | else: 226 | reports = reports - targeted_ignoreables 227 | if not reports: 228 | if cfg.get("verbose"): 229 | print( 230 | " Skipping check '%s', determined to only" 231 | " check ignoreable codes" % check_name 232 | ) 233 | continue 234 | if cfg.get("verbose"): 235 | print(" Running check '%s'" % check_name) 236 | if isinstance(c, checks.ContentCheck): 237 | for line_num, code, message in c.report_iter(f): 238 | if code in targeted_ignoreables: 239 | continue 240 | if not isinstance(line_num, (float, int)): 241 | line_num = "?" 242 | if cfg.get("verbose"): 243 | print( 244 | " - %s:%s: %s %s" % (f.filename, line_num, code, message) 245 | ) 246 | else: 247 | print("%s:%s: %s %s" % (f.filename, line_num, code, message)) 248 | error_counts[check_name] += 1 249 | elif isinstance(c, checks.LineCheck): 250 | for line_num, line in enumerate(f.lines_iter(), 1): 251 | for code, message in c.report_iter(line): 252 | if code in targeted_ignoreables: 253 | continue 254 | if cfg.get("verbose"): 255 | print( 256 | " - %s:%s: %s %s" 257 | % (f.filename, line_num, code, message) 258 | ) 259 | else: 260 | print( 261 | "%s:%s: %s %s" % (f.filename, line_num, code, message) 262 | ) 263 | error_counts[check_name] += 1 264 | else: 265 | raise TypeError("Unknown check type: %s, %s" % (type(c), c)) 266 | return error_counts 267 | 268 | 269 | def main(): 270 | parser = argparse.ArgumentParser( 271 | prog="doc8", 272 | description=__doc__, 273 | formatter_class=argparse.RawDescriptionHelpFormatter, 274 | ) 275 | default_configs = ", ".join(CONFIG_FILENAMES) 276 | parser.add_argument( 277 | "paths", 278 | metavar="path", 279 | type=str, 280 | nargs="*", 281 | help=("path to scan for doc files" " (default: current directory)."), 282 | default=[os.getcwd()], 283 | ) 284 | parser.add_argument( 285 | "--config", 286 | metavar="path", 287 | action="append", 288 | help="user config file location" " (default: %s)." % default_configs, 289 | default=[], 290 | ) 291 | parser.add_argument( 292 | "--allow-long-titles", 293 | action="store_true", 294 | help="allow long section titles (default: false).", 295 | default=False, 296 | ) 297 | parser.add_argument( 298 | "--ignore", 299 | action="append", 300 | metavar="code", 301 | help="ignore the given error code(s).", 302 | type=split_set_type, 303 | default=[], 304 | ) 305 | parser.add_argument( 306 | "--no-sphinx", 307 | action="store_false", 308 | help="do not ignore sphinx specific false positives.", 309 | default=True, 310 | dest="sphinx", 311 | ) 312 | parser.add_argument( 313 | "--ignore-path", 314 | action="append", 315 | default=[], 316 | help="ignore the given directory or file (globs" " are supported).", 317 | metavar="path", 318 | ) 319 | parser.add_argument( 320 | "--ignore-path-errors", 321 | action="append", 322 | default=[], 323 | help="ignore the given specific errors in the" " provided file.", 324 | metavar="path", 325 | ) 326 | parser.add_argument( 327 | "--default-extension", 328 | action="store", 329 | help="default file extension to use when a file is" 330 | " found without a file extension.", 331 | default="", 332 | dest="default_extension", 333 | metavar="extension", 334 | ) 335 | parser.add_argument( 336 | "--file-encoding", 337 | action="store", 338 | help="override encoding to use when attempting" 339 | " to determine an input files text encoding " 340 | "(providing this avoids using `chardet` to" 341 | " automatically detect encoding/s)", 342 | default="", 343 | dest="file_encoding", 344 | metavar="encoding", 345 | ) 346 | parser.add_argument( 347 | "--max-line-length", 348 | action="store", 349 | metavar="int", 350 | type=int, 351 | help="maximum allowed line" " length (default: %s)." % MAX_LINE_LENGTH, 352 | default=MAX_LINE_LENGTH, 353 | ) 354 | parser.add_argument( 355 | "-e", 356 | "--extension", 357 | action="append", 358 | metavar="extension", 359 | help="check file extensions of the given type" 360 | " (default: %s)." % ", ".join(FILE_PATTERNS), 361 | default=list(FILE_PATTERNS), 362 | ) 363 | parser.add_argument( 364 | "-q", 365 | "--quiet", 366 | action="store_true", 367 | help="only print violations", 368 | default=False, 369 | ) 370 | parser.add_argument( 371 | "-v", 372 | "--verbose", 373 | dest="verbose", 374 | action="store_true", 375 | help="run in verbose mode.", 376 | default=False, 377 | ) 378 | parser.add_argument( 379 | "--version", 380 | dest="version", 381 | action="store_true", 382 | help="show the version and exit.", 383 | default=False, 384 | ) 385 | args = vars(parser.parse_args()) 386 | if args.get("version"): 387 | print(version.version_string) 388 | return 0 389 | args["ignore"] = merge_sets(args["ignore"]) 390 | cfg = extract_config(args) 391 | args["ignore"].update(cfg.pop("ignore", set())) 392 | if "sphinx" in cfg: 393 | args["sphinx"] = cfg.pop("sphinx") 394 | args["extension"].extend(cfg.pop("extension", [])) 395 | args["ignore_path"].extend(cfg.pop("ignore_path", [])) 396 | 397 | cfg.setdefault("ignore_path_errors", {}) 398 | tmp_ignores = parse_ignore_path_errors(args.pop("ignore_path_errors", [])) 399 | for path, ignores in six.iteritems(tmp_ignores): 400 | if path in cfg["ignore_path_errors"]: 401 | cfg["ignore_path_errors"][path].update(ignores) 402 | else: 403 | cfg["ignore_path_errors"][path] = set(ignores) 404 | 405 | args.update(cfg) 406 | setup_logging(args.get("verbose")) 407 | 408 | files, files_ignored = scan(args) 409 | files_selected = len(files) 410 | error_counts = validate(args, files) 411 | total_errors = sum(six.itervalues(error_counts)) 412 | 413 | if not args.get("quiet"): 414 | print("=" * 8) 415 | print("Total files scanned = %s" % (files_selected)) 416 | print("Total files ignored = %s" % (files_ignored)) 417 | print("Total accumulated errors = %s" % (total_errors)) 418 | if error_counts: 419 | print("Detailed error counts:") 420 | for check_name in sorted(six.iterkeys(error_counts)): 421 | check_errors = error_counts[check_name] 422 | print(" - %s = %s" % (check_name, check_errors)) 423 | 424 | if total_errors: 425 | return 1 426 | else: 427 | return 0 428 | 429 | 430 | if __name__ == "__main__": 431 | sys.exit(main()) 432 | --------------------------------------------------------------------------------