diff --git a/.gitignore b/.gitignore index 320bdffff68c339fffb9e76544b377d02336dffd..f1fde8a77772b1125ad2e22dc998afe3fc70ae00 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,7 @@ coverage.xml # Sphinx documentation doc/build/ +# pbr stuff +AUTHORS +ChangeLog +.eggs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fa9b8c03d13e7be6e43255f249671650a44ec8ba --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks.git + rev: v2.3.0 # Use the ref you want to point at + hooks: + - id: trailing-whitespace + - id: check-yaml + - id: end-of-file-fixer + - id: flake8 + - id: trailing-whitespace + - id: check-executables-have-shebangs + - repo: https://github.com/python/black + rev: 19.3b0 + hooks: + - id: black + - repo: https://gitlab.com/pycqa/flake8.git + rev: 3.7.8 + hooks: + - id: flake8 diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e7194e80fd8c945679a103f33f7bbf7c2bd113fc --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,11 @@ +--- + +# For use with pre-commit. +# See usage instructions at http://pre-commit.com + +- id: doc8 + name: doc8 + description: This hook runs doc8 for linting docs + entry: python -m doc8 + language: python + files: \.rst$ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000000000000000000000000000000000..cfb8ad4c0ae0d49a69711ecbfd763e9fac34441d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,52 @@ +--- +dist: xenial +language: python +cache: + - pip + - directories: + - $HOME/.cache +os: + - linux +# tox 3.8.0 is the first version that can boostrap itself +before_script: + - pip install tox>=3.8.0 + +# test script +script: tox +notifications: + on_success: change + on_failure: always + +jobs: + fast_finish: true + include: + - name: lint,docs,py37 + env: TOXENV=lint,docs + python: "3.7" + - name: py36 + python: "3.6" + env: TOXENV=py36 + - name: py35 + python: "3.5" + env: TOXENV=py35 + - name: py27 + python: "2.7" + env: TOXENV=py27 + - name: packaging + python: "3.6" + env: TOXENV=packaging + +deploy: + provider: pypi + username: __token__ + password: + secure: "fSEPPa9lkEiqNZKMs0qCdI3gSWYSoqjj+gk33bvZXpWvIYkrRGNYt77be6lAdNX4SBW2LaaHmAd8WZ9YuaYTfTfNksn25mSjOKw3yEhmus9V7r5VmLyODIlhuDmn+RGenzf1wKEXKdBJQ8qXjzH1R2MbagIEjApyTWTYX4tfjoMR5v41g+wP4VC9wwjQx6q7oZhwQWj/9nSw8Ww45By1ozH2E4oT5bGSS/guXxuLsQ+oUfQfe+9Ht8kiT4n7RbfHyYQTe3VjV1IZ0hhiiA/SxF0UwsPt4Lr242W59TkaGayWjbSNCv7REQQXyHGKLcns5C50eD3Up6ZDmehOpiGf8TUCwb7FuAi71lD+Rr6Uu7IY44nUlsCDeQ9/sqKGWLsJSZgf4mBYJBqSx9GD+1eqmBPx/AONbVTUdhWh2Ve+JfXztfBeJpdKmmUsRC3CjqbIf8UKNDz5zcuUJ7yv4o0V+SunUDSdMOxkYaW9tP4YwJQqwCpNOi5R80JjhJq4bwvjdhGatC7oLDuoZ17R7b9OrBT8TpaXr/R+rgC66AHxmC/qgxZf4z7he+6vtmJPnVNUDzKj24t0wPVHprZkCSkLPf00gFU4UvG+J2OTCaHEImHtL5dp2wv+NgQbnm5d6xCUb80LW38AGx6E46potk+mnkMM1P4+W1tmtoYkDX6eIB0=" + edge: true # opt in to dpl v2 + distributions: "sdist bdist_wheel" + skip_existing: true + on: + tags: true + +env: + global: + - PIP_DISABLE_PIP_VERSION_CHECK=1 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5d73ec42cb68bea74ac0d9a0f4549c5a8bed668c..d05ed0834696775db18921020869fcc40f0fa0b9 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,15 +1,8 @@ -If you would like to contribute to the development of OpenStack, -you must follow the steps in this page: +Before contributing to *doc8* or any other PyCQA project, we suggest you read +the PyCQA meta documentation: - http://docs.openstack.org/infra/manual/developers.html + http://meta.pycqa.org/en/latest/ -Once those steps have been completed, changes to OpenStack -should be submitted for review via the Gerrit tool, following -the workflow documented at: +Patches for *doc8* should be submitted to GitHub, as should bugs: - http://docs.openstack.org/infra/manual/developers.html#development-workflow - -Pull requests submitted through GitHub will be ignored. -You can report the bugs at launchpad. - - https://bugs.launchpad.net/doc8 + https://github.com/pycqa/doc8 diff --git a/HACKING.rst b/HACKING.rst deleted file mode 100644 index b889622138c86f116fa71cc4c6a8436d73dde302..0000000000000000000000000000000000000000 --- a/HACKING.rst +++ /dev/null @@ -1,4 +0,0 @@ -doc8 Style Commandments -=============================================== - -Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ diff --git a/LICENSE b/LICENSE index ad410e11302107da9aa47ce3d46bd5ad011c4c43..5c304d1a4a7b439f767990bf1360d3283e45d0ee 100644 --- a/LICENSE +++ b/LICENSE @@ -198,4 +198,4 @@ Apache License distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 762d33b67a0fc73e55275db7ea28e721fcd0cfdc..0000000000000000000000000000000000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include README.rst - -exclude .gitignore -exclude .gitreview - -global-exclude *.pyc diff --git a/README.rst b/README.rst index fb7d0cee821a158ba4a0fc094be6341ea332c79f..5d1d4992c60e5dc070b9297ada3464688931c71d 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,13 @@ +.. image:: https://travis-ci.com/PyCQA/doc8.svg?branch=master + :target: https://travis-ci.com/PyCQA/doc8 +.. image:: https://img.shields.io/pypi/v/doc8 + :alt: PyPI + :target: https://pypi.org/project/doc8/ +.. image:: https://img.shields.io/pypi/l/doc8 + :alt: PyPI - License +.. image:: https://img.shields.io/github/last-commit/pycqa/doc8 + :alt: GitHub last commit + ==== Doc8 ==== @@ -104,6 +114,29 @@ An example section that can be placed into one of these files:: only variation of this being the ``no-sphinx`` option which from configuration file will be ``sphinx`` instead). +Python Usage +************ + +To call doc8 from a Python project:: + + from doc8 import doc8 + + result = doc8(allow_long_titles=True, max_line_length=99) + +The returned ``result`` will have the following attributes and methods: + +* ``result.files_selected`` - number of files selected +* ``result.files_ignored`` - number of files ignored +* ``result.error_counts`` - ``dict`` of ``{check_name: error_count}`` +* ``result.total_errors`` - total number of errors found +* ``result.errors`` - list of + ``(check_name, filename, line_num, code, message)`` tuples +* ``result.report()`` - returns a human-readable report as a string + +Note that calling ``doc8`` in this way will not write to stdout, so the +``quiet`` and ``verbose`` options are ignored. + + Option conflict resolution ************************** diff --git a/debian/changelog b/debian/changelog index a9b25aeb59542a6a67927ec490276cd9f6de5c29..55791ccdb874021a371348b9919969cf040c9e7a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,9 +1,20 @@ -python-doc8 (0.8.0-5) UNRELEASED; urgency=medium +python-doc8 (0.8.1~rc3-1) UNRELEASED; urgency=medium + [ Ondřej Nový ] * Use debhelper-compat instead of debian/compat. * Bump Standards-Version to 4.4.1. - -- Ondřej Nový Thu, 18 Jul 2019 16:40:19 +0200 + [ Jordan Justen ] + * New upstream release. (Closes: #955070) + * d/control: Move upstream webpage to new one listed on the old page + * d/watch: Update watch file url and watch format + * d/control: Add python3-mock build dep + * d/control: Add python3-sphinx-rtd-theme build dep + * d/patches: Remove upstream badges from README for lintian privacy-breach-generic + * d/control: Update Standards-Version to 4.5.0 + * d/control: Remove python3-oslosphinx build dep + + -- Jordan Justen Sun, 19 Apr 2020 23:18:19 -0700 python-doc8 (0.8.0-4) unstable; urgency=medium diff --git a/debian/control b/debian/control index 952ea0e1e895d127612dbef84538bbcda5de418e..b5bf0de1f149d7cf39d8bceeccc1fce0510903bc 100644 --- a/debian/control +++ b/debian/control @@ -16,16 +16,17 @@ Build-Depends-Indep: python3-chardet, python3-docutils, python3-hacking, + python3-mock, python3-nose, - python3-oslosphinx, python3-restructuredtext-lint, python3-six, + python3-sphinx-rtd-theme, python3-stevedore, python3-testtools, -Standards-Version: 4.4.1 +Standards-Version: 4.5.0 Vcs-Browser: https://salsa.debian.org/openstack-team/libs/python-doc8 Vcs-Git: https://salsa.debian.org/openstack-team/libs/python-doc8.git -Homepage: https://git.openstack.org/cgit/openstack/doc8 +Homepage: https://github.com/PyCQA/doc8 Package: python-doc8-doc Section: doc diff --git a/debian/patches/0001-Revert-Added-badges-to-the-README.patch b/debian/patches/0001-Revert-Added-badges-to-the-README.patch new file mode 100644 index 0000000000000000000000000000000000000000..ebe0d525994bc75bcd6466a0b4301a17a036a91f --- /dev/null +++ b/debian/patches/0001-Revert-Added-badges-to-the-README.patch @@ -0,0 +1,27 @@ +From: Jordan Justen +Date: Sun, 19 Apr 2020 02:18:56 -0700 +Subject: Revert "Added badges to the README" + +This reverts commit 2adc4fdc36d0e16b570ba832466f2a806799c23f. +--- + README.rst | 10 ---------- + 1 file changed, 10 deletions(-) + +diff --git a/README.rst b/README.rst +index 5d1d499..52e72fc 100644 +--- a/README.rst ++++ b/README.rst +@@ -1,13 +1,3 @@ +-.. image:: https://travis-ci.com/PyCQA/doc8.svg?branch=master +- :target: https://travis-ci.com/PyCQA/doc8 +-.. image:: https://img.shields.io/pypi/v/doc8 +- :alt: PyPI +- :target: https://pypi.org/project/doc8/ +-.. image:: https://img.shields.io/pypi/l/doc8 +- :alt: PyPI - License +-.. image:: https://img.shields.io/github/last-commit/pycqa/doc8 +- :alt: GitHub last commit +- + ==== + Doc8 + ==== diff --git a/debian/patches/series b/debian/patches/series new file mode 100644 index 0000000000000000000000000000000000000000..b8e13a37a444991c5781d82eccf4771f2c83beaf --- /dev/null +++ b/debian/patches/series @@ -0,0 +1 @@ +0001-Revert-Added-badges-to-the-README.patch diff --git a/debian/watch b/debian/watch index 3ad2e410efd686da0bf2af17de678aaa0bfa609f..068acf17299bec4c01fa39ddd1f04f7e2f390857 100644 --- a/debian/watch +++ b/debian/watch @@ -1,3 +1,5 @@ -version=3 -https://github.com/stackforge/doc8/tags .*/(\d[\d\.]+)\.tar\.gz - +version=4 +opts=\ +uversionmangle=s/(\d)[_\.\-\+]?((RC|rc|pre|dev|beta|alpha)\d*)$/$1~$2/,\ +filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/doc8-$1\.tar\.gz/ \ + https://github.com/PyCQA/doc8/tags .*/v?(\d\S+)\.tar\.gz diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..86ac8a4af6811e382e521f3367a4b1e594ce7ef5 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,3 @@ +pbr # Apache +sphinx>=1.8.0 # BSD +sphinx_rtd_theme>=0.4.0 # MIT diff --git a/doc/source/conf.py b/doc/source/conf.py old mode 100755 new mode 100644 index 57e7215883fa46bcb2affe44a51273080dc32408..59841805630dbc3c3c595e86c8146365d31a5b68 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -11,60 +11,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import sys - -sys.path.insert(0, os.path.abspath('../..')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [ - 'sphinx.ext.autodoc', - 'oslosphinx' -] - -# autodoc generation is a bit aggressive and a nuisance when doing heavy -# text edit cycles. -# execute "export SPHINX_DEBUG=1" in your terminal to disable - -# The suffix of source filenames. -source_suffix = '.rst' +extensions = ["sphinx.ext.autodoc"] # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'doc8' -copyright = u'2013, OpenStack Foundation' - -# If true, '()' will be appended to :func: etc. cross-reference text. -add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -add_module_names = True +project = u"doc8" +copyright = u"2013, OpenStack Foundation" # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -# html_theme_path = ["."] -# html_theme = '_theme' -# html_static_path = ['static'] - -# Output file base name for HTML help builder. -htmlhelp_basename = '%sdoc' % project - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass -# [howto/manual]). -latex_documents = [ - ('index', - '%s.tex' % project, - u'%s Documentation' % project, - u'OpenStack Foundation', 'manual'), -] +html_theme = "sphinx_rtd_theme" diff --git a/doc/source/index.rst b/doc/source/index.rst index 4628227ce1046f788a6039d70409c4d1d2c4e62f..b0d8019fd8d45a4d9751ce0130b33b78bf99cf8e 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -8,7 +8,6 @@ Contents: readme installation - usage contributing Indices and tables @@ -17,4 +16,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/doc/source/usage.rst b/doc/source/usage.rst deleted file mode 100644 index 0f8ec5d6b2dcb27869b4cc90edee104dfd7b8ce8..0000000000000000000000000000000000000000 --- a/doc/source/usage.rst +++ /dev/null @@ -1,7 +0,0 @@ -===== -Usage -===== - -To use doc8 in a project:: - - import doc8 diff --git a/doc8/__init__.py b/doc8/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..75f40db3583327ad9927c3cdb4d359d366fe3481 100644 --- a/doc8/__init__.py +++ b/doc8/__init__.py @@ -0,0 +1 @@ +from .main import doc8 # noqa diff --git a/doc8/__main__.py b/doc8/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b491028670961fb4027ea0be6d4518d272a3d1c --- /dev/null +++ b/doc8/__main__.py @@ -0,0 +1,20 @@ +# Copyright (C) 2019 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys + +from doc8 import main + + +sys.exit(main.main()) diff --git a/doc8/checks.py b/doc8/checks.py index cf3a06d83ff4420b75362bc20b1a21a6525561c3..b7cb780b04637814c45a5d85cc3244d8b9a8e4f4 100644 --- a/doc8/checks.py +++ b/doc8/checks.py @@ -1,7 +1,5 @@ # Copyright (C) 2014 Ivan Melnikov # -# Author: Joshua Harlow -# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -45,24 +43,24 @@ class LineCheck(object): class CheckTrailingWhitespace(LineCheck): - _TRAILING_WHITESPACE_REGEX = re.compile('\s$') + _TRAILING_WHITESPACE_REGEX = re.compile(r"\s$") REPORTS = frozenset(["D002"]) def report_iter(self, line): if self._TRAILING_WHITESPACE_REGEX.search(line): - yield ('D002', 'Trailing whitespace') + yield ("D002", "Trailing whitespace") class CheckIndentationNoTab(LineCheck): - _STARTING_WHITESPACE_REGEX = re.compile('^(\s+)') + _STARTING_WHITESPACE_REGEX = re.compile(r"^(\s+)") REPORTS = frozenset(["D003"]) def report_iter(self, line): match = self._STARTING_WHITESPACE_REGEX.search(line) if match: spaces = match.group(1) - if '\t' in spaces: - yield ('D003', 'Tabulation used for indentation') + if "\t" in spaces: + yield ("D003", "Tabulation used for indentation") class CheckCarriageReturn(LineCheck): @@ -70,7 +68,7 @@ class CheckCarriageReturn(LineCheck): def report_iter(self, line): if "\r" in line: - yield ('D004', 'Found literal carriage return') + yield ("D004", "Found literal carriage return") class CheckNewlineEndOfFile(ContentCheck): @@ -80,8 +78,8 @@ class CheckNewlineEndOfFile(ContentCheck): super(CheckNewlineEndOfFile, self).__init__(cfg) def report_iter(self, parsed_file): - if parsed_file.lines and not parsed_file.lines[-1].endswith(b'\n'): - yield (len(parsed_file.lines), 'D005', 'No newline at end of file') + if parsed_file.lines and not parsed_file.lines[-1].endswith(b"\n"): + yield (len(parsed_file.lines), "D005", "No newline at end of file") class CheckValidity(ContentCheck): @@ -98,15 +96,23 @@ class CheckValidity(ContentCheck): # Only used when running in sphinx mode. SPHINX_IGNORES_REGEX = [ - re.compile(r'^Unknown interpreted text'), - re.compile(r'^Unknown directive type'), - re.compile(r'^Undefined substitution'), - re.compile(r'^Substitution definition contains illegal element'), + re.compile(r"^Unknown interpreted text"), + re.compile(r"^Unknown directive type"), + re.compile(r"^Undefined substitution"), + re.compile(r"^Substitution definition contains illegal element"), + re.compile( + r'^Error in \"code-block\" directive\:\nunknown option: "caption".', + re.MULTILINE, + ), + re.compile( + r'^Error in "code-block" directive:\nunknown option: "emphasize-lines"' + ), + re.compile(r'^Error in "math" directive:\nunknown option: "label"'), ] def __init__(self, cfg): super(CheckValidity, self).__init__(cfg) - self._sphinx_mode = cfg.get('sphinx') + self._sphinx_mode = cfg.get("sphinx") def report_iter(self, parsed_file): for error in parsed_file.errors: @@ -119,7 +125,7 @@ class CheckValidity(ContentCheck): ignore = True break if not ignore: - yield (error.line, 'D000', error.message) + yield (error.line, "D000", error.message) class CheckMaxLineLength(ContentCheck): @@ -127,11 +133,10 @@ class CheckMaxLineLength(ContentCheck): def __init__(self, cfg): super(CheckMaxLineLength, self).__init__(cfg) - self._max_line_length = self._cfg['max_line_length'] - self._allow_long_titles = self._cfg['allow_long_titles'] + self._max_line_length = self._cfg["max_line_length"] + self._allow_long_titles = self._cfg["allow_long_titles"] def _extract_node_lines(self, doc): - def extract_lines(node, start_line): lines = [start_line] if isinstance(node, (docutils_nodes.title)): @@ -171,12 +176,10 @@ class CheckMaxLineLength(ContentCheck): if first_line == -1: first_line = line contained_lines = set(gather_lines(n)) - nodes_lines.append((n, (min(contained_lines), - max(contained_lines)))) + nodes_lines.append((n, (min(contained_lines), max(contained_lines)))) return (nodes_lines, first_line) def _extract_directives(self, lines): - def starting_whitespace(line): m = re.match(r"^(\s+)(.*)$", line) if not m: @@ -187,7 +190,7 @@ class CheckMaxLineLength(ContentCheck): return bool(re.match(r"^(\s*)$", line)) def find_directive_end(start, lines): - after_lines = collections.deque(lines[start + 1:]) + after_lines = collections.deque(lines[start + 1 :]) k = 0 while after_lines: line = after_lines.popleft() @@ -220,8 +223,9 @@ class CheckMaxLineLength(ContentCheck): # if line is a list, line is checked as normal line if re.match(listspattern, line): continue - if (len(re.search(lwhitespaces, line).group()) < - len(re.search(lwhitespaces, next_line).group())): + if len(re.search(lwhitespaces, line).group()) < len( + re.search(lwhitespaces, next_line).group() + ): directives.append((i, i)) return directives @@ -230,7 +234,7 @@ class CheckMaxLineLength(ContentCheck): for i, line in enumerate(parsed_file.lines_iter()): if len(line) > self._max_line_length: if not utils.contains_url(line): - yield (i + 1, 'D001', 'Line too long') + yield (i + 1, "D001", "Line too long") def _rst_checker(self, parsed_file): lines = list(parsed_file.lines_iter()) @@ -262,10 +266,7 @@ class CheckMaxLineLength(ContentCheck): def any_types(nodes, types): return any([isinstance(n, types) for n in nodes]) - skip_types = ( - docutils_nodes.target, - docutils_nodes.literal_block, - ) + skip_types = (docutils_nodes.target, docutils_nodes.literal_block) title_types = ( docutils_nodes.title, docutils_nodes.subtitle, @@ -281,7 +282,7 @@ class CheckMaxLineLength(ContentCheck): if in_directive: continue stripped = line.lstrip() - if ' ' not in stripped: + if " " not in stripped: # No room to split even if we could. continue if utils.contains_url(stripped): @@ -291,10 +292,10 @@ class CheckMaxLineLength(ContentCheck): continue if self._allow_long_titles and any_types(nodes, title_types): continue - yield (i + 1, 'D001', 'Line too long') + yield (i + 1, "D001", "Line too long") def report_iter(self, parsed_file): - if parsed_file.extension.lower() != '.rst': + if parsed_file.extension.lower() != ".rst": checker_func = self._txt_checker else: checker_func = self._rst_checker diff --git a/doc8/main.py b/doc8/main.py index 9fc10e211b05e4fa33f37da194b51145c22a8c23..cc7155f2caa0335870ddd546af0aa13a024841fc 100644 --- a/doc8/main.py +++ b/doc8/main.py @@ -1,7 +1,5 @@ # Copyright (C) 2014 Ivan Melnikov # -# Author: Joshua Harlow -# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -36,11 +34,6 @@ import logging import os import sys -if __name__ == '__main__': - # Only useful for when running directly (for dev/debugging). - sys.path.insert(0, os.path.abspath(os.getcwd())) - sys.path.insert(0, os.path.abspath(os.path.join(os.pardir, os.getcwd()))) - import six from six.moves import configparser from stevedore import extension @@ -50,14 +43,9 @@ from doc8 import parser as file_parser from doc8 import utils from doc8 import version -FILE_PATTERNS = ['.rst', '.txt'] +FILE_PATTERNS = [".rst", ".txt"] MAX_LINE_LENGTH = 79 -CONFIG_FILENAMES = [ - "doc8.ini", - "tox.ini", - "pep8.ini", - "setup.cfg", -] +CONFIG_FILENAMES = ["doc8.ini", "tox.ini", "pep8.ini", "setup.cfg"] def split_set_type(text, delimiter=","): @@ -84,9 +72,9 @@ def parse_ignore_path_errors(entries): def extract_config(args): parser = configparser.RawConfigParser() read_files = [] - if args['config']: - for fn in args['config']: - with open(fn, 'r') as fh: + if args["config"]: + for fn in args["config"]: + with open(fn, "r") as fh: parser.readfp(fh, filename=fn) read_files.append(fn) else: @@ -95,44 +83,42 @@ def extract_config(args): return {} cfg = {} try: - cfg['max_line_length'] = parser.getint("doc8", "max-line-length") + cfg["max_line_length"] = parser.getint("doc8", "max-line-length") except (configparser.NoSectionError, configparser.NoOptionError): pass try: - cfg['ignore'] = split_set_type(parser.get("doc8", "ignore")) + cfg["ignore"] = split_set_type(parser.get("doc8", "ignore")) except (configparser.NoSectionError, configparser.NoOptionError): pass try: - cfg['ignore_path'] = split_set_type(parser.get("doc8", - "ignore-path")) + cfg["ignore_path"] = split_set_type(parser.get("doc8", "ignore-path")) except (configparser.NoSectionError, configparser.NoOptionError): pass try: ignore_path_errors = parser.get("doc8", "ignore-path-errors") ignore_path_errors = split_set_type(ignore_path_errors) ignore_path_errors = parse_ignore_path_errors(ignore_path_errors) - cfg['ignore_path_errors'] = ignore_path_errors + cfg["ignore_path_errors"] = ignore_path_errors except (configparser.NoSectionError, configparser.NoOptionError): pass try: - cfg['allow_long_titles'] = parser.getboolean("doc8", - "allow-long-titles") + cfg["allow_long_titles"] = parser.getboolean("doc8", "allow-long-titles") except (configparser.NoSectionError, configparser.NoOptionError): pass try: - cfg['sphinx'] = parser.getboolean("doc8", "sphinx") + cfg["sphinx"] = parser.getboolean("doc8", "sphinx") except (configparser.NoSectionError, configparser.NoOptionError): pass try: - cfg['verbose'] = parser.getboolean("doc8", "verbose") + cfg["verbose"] = parser.getboolean("doc8", "verbose") except (configparser.NoSectionError, configparser.NoOptionError): pass try: - cfg['file_encoding'] = parser.get("doc8", "file-encoding") + cfg["file_encoding"] = parser.get("doc8", "file-encoding") except (configparser.NoSectionError, configparser.NoOptionError): pass try: - cfg['default_extension'] = parser.get("doc8", "default-extension") + cfg["default_extension"] = parser.get("doc8", "default-extension") except (configparser.NoSectionError, configparser.NoOptionError): pass try: @@ -140,7 +126,7 @@ def extract_config(args): extensions = extensions.split(",") extensions = [s.strip() for s in extensions if s.strip()] if extensions: - cfg['extension'] = extensions + cfg["extension"] = extensions except (configparser.NoSectionError, configparser.NoOptionError): pass return cfg @@ -156,9 +142,7 @@ def fetch_checks(cfg): checks.CheckNewlineEndOfFile(cfg), ] mgr = extension.ExtensionManager( - namespace='doc8.extension.check', - invoke_on_load=True, - invoke_args=(cfg.copy(),), + namespace="doc8.extension.check", invoke_on_load=True, invoke_args=(cfg.copy(),) ) addons = [] for e in mgr: @@ -171,54 +155,51 @@ def setup_logging(verbose): level = logging.DEBUG else: level = logging.ERROR - logging.basicConfig(level=level, - format='%(levelname)s: %(message)s', stream=sys.stdout) + logging.basicConfig( + level=level, format="%(levelname)s: %(message)s", stream=sys.stdout + ) def scan(cfg): - if not cfg.get('quiet'): + if not cfg.get("quiet"): print("Scanning...") files = collections.deque() - ignored_paths = cfg.get('ignore_path', []) + ignored_paths = cfg.get("ignore_path", []) files_ignored = 0 - file_iter = utils.find_files(cfg.get('paths', []), - cfg.get('extension', []), ignored_paths) - default_extension = cfg.get('default_extension') - file_encoding = cfg.get('file_encoding') + file_iter = utils.find_files( + cfg.get("paths", []), cfg.get("extension", []), ignored_paths + ) + default_extension = cfg.get("default_extension") + file_encoding = cfg.get("file_encoding") for filename, ignoreable in file_iter: if ignoreable: files_ignored += 1 - if cfg.get('verbose'): + if cfg.get("verbose"): print(" Ignoring '%s'" % (filename)) else: - f = file_parser.parse(filename, - default_extension=default_extension, - encoding=file_encoding) + f = file_parser.parse( + filename, default_extension=default_extension, encoding=file_encoding + ) files.append(f) - if cfg.get('verbose'): + if cfg.get("verbose"): print(" Selecting '%s'" % (filename)) return (files, files_ignored) -def validate(cfg, files): - if not cfg.get('quiet'): +def validate(cfg, files, result=None): + if not cfg.get("quiet"): print("Validating...") error_counts = {} - ignoreables = frozenset(cfg.get('ignore', [])) - ignore_targeted = cfg.get('ignore_path_errors', {}) + ignoreables = frozenset(cfg.get("ignore", [])) + ignore_targeted = cfg.get("ignore_path_errors", {}) while files: f = files.popleft() - if cfg.get('verbose'): + if cfg.get("verbose"): print("Validating %s" % f) targeted_ignoreables = set(ignore_targeted.get(f.filename, set())) targeted_ignoreables.update(ignoreables) for c in fetch_checks(cfg): - try: - # http://legacy.python.org/dev/peps/pep-3155/ - check_name = c.__class__.__qualname__ - except AttributeError: - check_name = ".".join([c.__class__.__module__, - c.__class__.__name__]) + check_name = ".".join([c.__class__.__module__, c.__class__.__name__]) error_counts.setdefault(check_name, 0) try: extension_matcher = c.EXT_MATCHER @@ -226,10 +207,12 @@ def validate(cfg, files): pass else: if not extension_matcher.match(f.extension): - if cfg.get('verbose'): - print(" Skipping check '%s' since it does not" - " understand parsing a file with extension '%s'" - % (check_name, f.extension)) + if cfg.get("verbose"): + print( + " Skipping check '%s' since it does not" + " understand parsing a file with extension '%s'" + % (check_name, f.extension) + ) continue try: reports = set(c.REPORTS) @@ -238,11 +221,13 @@ def validate(cfg, files): else: reports = reports - targeted_ignoreables if not reports: - if cfg.get('verbose'): - print(" Skipping check '%s', determined to only" - " check ignoreable codes" % check_name) + if cfg.get("verbose"): + print( + " Skipping check '%s', determined to only" + " check ignoreable codes" % check_name + ) continue - if cfg.get('verbose'): + if cfg.get("verbose"): print(" Running check '%s'" % check_name) if isinstance(c, checks.ContentCheck): for line_num, code, message in c.report_iter(f): @@ -250,129 +235,267 @@ def validate(cfg, files): continue if not isinstance(line_num, (float, int)): line_num = "?" - if cfg.get('verbose'): - print(' - %s:%s: %s %s' - % (f.filename, line_num, code, message)) - else: - print('%s:%s: %s %s' - % (f.filename, line_num, code, message)) + if cfg.get("verbose"): + print( + " - %s:%s: %s %s" % (f.filename, line_num, code, message) + ) + elif not result.capture: + print("%s:%s: %s %s" % (f.filename, line_num, code, message)) + result.error(check_name, f.filename, line_num, code, message) error_counts[check_name] += 1 elif isinstance(c, checks.LineCheck): for line_num, line in enumerate(f.lines_iter(), 1): for code, message in c.report_iter(line): if code in targeted_ignoreables: continue - if cfg.get('verbose'): - print(' - %s:%s: %s %s' - % (f.filename, line_num, code, message)) - else: - print('%s:%s: %s %s' - % (f.filename, line_num, code, message)) + if cfg.get("verbose"): + print( + " - %s:%s: %s %s" + % (f.filename, line_num, code, message) + ) + elif not result.capture: + print( + "%s:%s: %s %s" % (f.filename, line_num, code, message) + ) + result.error(check_name, f.filename, line_num, code, message) error_counts[check_name] += 1 else: - raise TypeError("Unknown check type: %s, %s" - % (type(c), c)) + raise TypeError("Unknown check type: %s, %s" % (type(c), c)) return error_counts -def main(): - parser = argparse.ArgumentParser( - prog='doc8', - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter) - default_configs = ", ".join(CONFIG_FILENAMES) - parser.add_argument("paths", metavar='path', type=str, nargs='*', - help=("path to scan for doc files" - " (default: current directory)."), - default=[os.getcwd()]) - parser.add_argument("--config", metavar='path', action="append", - help="user config file location" - " (default: %s)." % default_configs, - default=[]) - parser.add_argument("--allow-long-titles", action="store_true", - help="allow long section titles (default: false).", - default=False) - parser.add_argument("--ignore", action="append", metavar="code", - help="ignore the given error code(s).", - type=split_set_type, - default=[]) - parser.add_argument("--no-sphinx", action="store_false", - help="do not ignore sphinx specific false positives.", - default=True, dest='sphinx') - parser.add_argument("--ignore-path", action="append", default=[], - help="ignore the given directory or file (globs" - " are supported).", metavar='path') - parser.add_argument("--ignore-path-errors", action="append", default=[], - help="ignore the given specific errors in the" - " provided file.", metavar='path') - parser.add_argument("--default-extension", action="store", - help="default file extension to use when a file is" - " found without a file extension.", - default='', dest='default_extension', - metavar='extension') - parser.add_argument("--file-encoding", action="store", - help="override encoding to use when attempting" - " to determine an input files text encoding " - "(providing this avoids using `chardet` to" - " automatically detect encoding/s)", - default='', dest='file_encoding', - metavar='encoding') - parser.add_argument("--max-line-length", action="store", metavar="int", - type=int, - help="maximum allowed line" - " length (default: %s)." % MAX_LINE_LENGTH, - default=MAX_LINE_LENGTH) - parser.add_argument("-e", "--extension", action="append", - metavar="extension", - help="check file extensions of the given type" - " (default: %s)." % ", ".join(FILE_PATTERNS), - default=list(FILE_PATTERNS)) - parser.add_argument("-q", "--quiet", action='store_true', - help="only print violations", default=False) - parser.add_argument("-v", "--verbose", dest="verbose", action='store_true', - help="run in verbose mode.", default=False) - parser.add_argument("--version", dest="version", action='store_true', - help="show the version and exit.", default=False) - args = vars(parser.parse_args()) - if args.get('version'): - print(version.version_string()) - return 0 - args['ignore'] = merge_sets(args['ignore']) +def get_defaults(): + return { + "paths": [os.getcwd()], + "config": [], + "allow_long_titles": False, + "ignore": [], + "sphinx": True, + "ignore_path": [], + "ignore_path_errors": [], + "default_extension": "", + "file_encoding": "", + "max_line_length": MAX_LINE_LENGTH, + "extension": list(FILE_PATTERNS), + "quiet": False, + "verbose": False, + "version": False, + } + + +class Result(object): + def __init__(self): + self.files_selected = 0 + self.files_ignored = 0 + self.error_counts = {} + self.errors = [] + self.capture = False + + @property + def total_errors(self): + return len(self.errors) + + def error(self, check_name, filename, line_num, code, message): + self.errors.append((check_name, filename, line_num, code, message)) + + def finish(self, files_selected, files_ignored, error_counts): + self.files_selected = files_selected + self.files_ignored = files_ignored + self.error_counts = error_counts + + def report(self): + lines = [] + if self.capture: + for error in self.errors: + lines.append("%s:%s: %s %s" % error[1:]) + + lines.extend( + [ + "=" * 8, + "Total files scanned = %s" % (self.files_selected), + "Total files ignored = %s" % (self.files_ignored), + "Total accumulated errors = %s" % (self.total_errors), + ] + ) + + if self.error_counts: + lines.append("Detailed error counts:") + for check_name in sorted(six.iterkeys(self.error_counts)): + check_errors = self.error_counts[check_name] + lines.append(" - %s = %s" % (check_name, check_errors)) + + return "\n".join(lines) + + +def doc8(args=None, **kwargs): + result = Result() + if args is None: + args = get_defaults() + + # Force reporting to suppress all output + kwargs["quiet"] = True + kwargs["verbose"] = False + result.capture = True + + args["ignore"] = merge_sets(args["ignore"]) cfg = extract_config(args) - args['ignore'].update(cfg.pop("ignore", set())) - if 'sphinx' in cfg: - args['sphinx'] = cfg.pop("sphinx") - args['extension'].extend(cfg.pop('extension', [])) - args['ignore_path'].extend(cfg.pop('ignore_path', [])) - - cfg.setdefault('ignore_path_errors', {}) - tmp_ignores = parse_ignore_path_errors(args.pop('ignore_path_errors', [])) + args["ignore"].update(cfg.pop("ignore", set())) + if "sphinx" in cfg: + args["sphinx"] = cfg.pop("sphinx") + args["extension"].extend(cfg.pop("extension", [])) + args["ignore_path"].extend(cfg.pop("ignore_path", [])) + + cfg.setdefault("ignore_path_errors", {}) + tmp_ignores = parse_ignore_path_errors(args.pop("ignore_path_errors", [])) for path, ignores in six.iteritems(tmp_ignores): - if path in cfg['ignore_path_errors']: - cfg['ignore_path_errors'][path].update(ignores) + if path in cfg["ignore_path_errors"]: + cfg["ignore_path_errors"][path].update(ignores) else: - cfg['ignore_path_errors'][path] = set(ignores) + cfg["ignore_path_errors"][path] = set(ignores) args.update(cfg) - setup_logging(args.get('verbose')) + + # Override args with any kwargs + args.update(kwargs.items()) + setup_logging(args.get("verbose")) files, files_ignored = scan(args) files_selected = len(files) - error_counts = validate(args, files) - total_errors = sum(six.itervalues(error_counts)) - - if not args.get('quiet'): - print("=" * 8) - print("Total files scanned = %s" % (files_selected)) - print("Total files ignored = %s" % (files_ignored)) - print("Total accumulated errors = %s" % (total_errors)) - if error_counts: - print("Detailed error counts:") - for check_name in sorted(six.iterkeys(error_counts)): - check_errors = error_counts[check_name] - print(" - %s = %s" % (check_name, check_errors)) - - if total_errors: + + error_counts = validate(args, files, result=result) + result.finish(files_selected, files_ignored, error_counts) + return result + + +def main(): + defaults = get_defaults() + parser = argparse.ArgumentParser( + prog="doc8", + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "paths", + metavar="path", + type=str, + nargs="*", + help=("path to scan for doc files (default: current directory)."), + default=defaults["paths"], + ) + parser.add_argument( + "--config", + metavar="path", + action="append", + help="user config file location" + " (default: %s)." % ", ".join(CONFIG_FILENAMES), + default=defaults["config"], + ) + parser.add_argument( + "--allow-long-titles", + action="store_true", + help="allow long section titles (default: false).", + default=defaults["allow_long_titles"], + ) + parser.add_argument( + "--ignore", + action="append", + metavar="code", + help="ignore the given error code(s).", + type=split_set_type, + default=defaults["ignore"], + ) + parser.add_argument( + "--no-sphinx", + action="store_false", + help="do not ignore sphinx specific false positives.", + default=defaults["sphinx"], + dest="sphinx", + ) + parser.add_argument( + "--ignore-path", + action="append", + default=defaults["ignore_path"], + help="ignore the given directory or file (globs are supported).", + metavar="path", + ) + parser.add_argument( + "--ignore-path-errors", + action="append", + default=defaults["ignore_path_errors"], + help="ignore the given specific errors in the provided file.", + metavar="path", + ) + parser.add_argument( + "--default-extension", + action="store", + help="default file extension to use when a file is" + " found without a file extension.", + default=defaults["default_extension"], + dest="default_extension", + metavar="extension", + ) + parser.add_argument( + "--file-encoding", + action="store", + help="override encoding to use when attempting" + " to determine an input files text encoding " + "(providing this avoids using `chardet` to" + " automatically detect encoding/s)", + default=defaults["file_encoding"], + dest="file_encoding", + metavar="encoding", + ) + parser.add_argument( + "--max-line-length", + action="store", + metavar="int", + type=int, + help="maximum allowed line" + " length (default: %s)." % defaults["max_line_length"], + default=defaults["max_line_length"], + ) + parser.add_argument( + "-e", + "--extension", + action="append", + metavar="extension", + help="check file extensions of the given type" + " (default: %s)." % ", ".join(defaults["extension"]), + default=defaults["extension"], + ) + parser.add_argument( + "-q", + "--quiet", + action="store_true", + help="only print violations", + default=defaults["quiet"], + ) + parser.add_argument( + "-v", + "--verbose", + dest="verbose", + action="store_true", + help="run in verbose mode.", + default=defaults["verbose"], + ) + parser.add_argument( + "--version", + dest="version", + action="store_true", + help="show the version and exit.", + default=defaults["version"], + ) + args = vars(parser.parse_args()) + if args.get("version"): + print(version.version_string) + return 0 + + result = doc8(args) + + if not args.get("quiet"): + print(result.report()) + + if result.total_errors: return 1 else: return 0 diff --git a/doc8/parser.py b/doc8/parser.py index 0c1c57580bf2f95e0947d81d6501cb6975bd6730..d7d9f2dbb86dff95b9893108e1c081f352adcc43 100644 --- a/doc8/parser.py +++ b/doc8/parser.py @@ -1,7 +1,5 @@ # Copyright (C) 2014 Ivan Melnikov # -# Author: Joshua Harlow -# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -27,9 +25,9 @@ import six class ParsedFile(object): - FALLBACK_ENCODING = 'utf-8' + FALLBACK_ENCODING = "utf-8" - def __init__(self, filename, encoding=None, default_extension=''): + def __init__(self, filename, encoding=None, default_extension=""): self._filename = filename self._content = None self._raw_content = None @@ -60,19 +58,20 @@ class ParsedFile(object): parser_cls = docutils_parser.get_parser_class("rst") parser = parser_cls() defaults = { - 'halt_level': 5, - 'report_level': 5, - 'quiet': True, - 'file_insertion_enabled': False, - 'traceback': True, + "halt_level": 5, + "report_level": 5, + "quiet": True, + "file_insertion_enabled": False, + "traceback": True, # Development use only. - 'dump_settings': False, - 'dump_internals': False, - 'dump_transforms': False, + "dump_settings": False, + "dump_internals": False, + "dump_transforms": False, } opt = frontend.OptionParser(components=[parser], defaults=defaults) - doc = utils.new_document(source_path=self.filename, - settings=opt.get_default_values()) + doc = utils.new_document( + source_path=self.filename, settings=opt.get_default_values() + ) parser.parse(self.contents, doc) self._doc = doc return self._doc @@ -82,7 +81,7 @@ class ParsedFile(object): return with self._read_lock: if not self._has_read: - with open(self.filename, 'rb') as fh: + with open(self.filename, "rb") as fh: self._lines = list(fh) fh.seek(0) self._raw_content = fh.read() @@ -112,7 +111,7 @@ class ParsedFile(object): @property def encoding(self): if not self._encoding: - encoding = chardet.detect(self.raw_contents)['encoding'] + encoding = chardet.detect(self.raw_contents)["encoding"] if not encoding: encoding = self.FALLBACK_ENCODING self._encoding = encoding @@ -126,19 +125,19 @@ class ParsedFile(object): @property def contents(self): if self._content is None: - self._content = six.text_type(self.raw_contents, - encoding=self.encoding) + self._content = six.text_type(self.raw_contents, encoding=self.encoding) return self._content def __str__(self): return "%s (%s, %s chars, %s lines)" % ( - self.filename, self.encoding, len(self.contents), - len(list(self.lines_iter()))) + self.filename, + self.encoding, + len(self.contents), + len(list(self.lines_iter())), + ) -def parse(filename, encoding=None, default_extension=''): +def parse(filename, encoding=None, default_extension=""): if not os.path.isfile(filename): - raise IOError(errno.ENOENT, 'File not found', filename) - return ParsedFile(filename, - encoding=encoding, - default_extension=default_extension) + raise IOError(errno.ENOENT, "File not found", filename) + return ParsedFile(filename, encoding=encoding, default_extension=default_extension) diff --git a/doc8/tests/test_checks.py b/doc8/tests/test_checks.py index af32c0581d8153c3feb3f90b3cc1f6ce731c0bb2..8b9f096f1e4211b1cf759bf89f16d557d81feddb 100644 --- a/doc8/tests/test_checks.py +++ b/doc8/tests/test_checks.py @@ -73,11 +73,8 @@ test content += b"\n\n" content += (b"a" * 60) + b" " + (b"b" * 60) content += b"\n" - conf = { - 'max_line_length': 79, - 'allow_long_titles': True, - } - for ext in ['.rst', '.txt']: + conf = {"max_line_length": 79, "allow_long_titles": True} + for ext in [".rst", ".txt"]: with tempfile.NamedTemporaryFile(suffix=ext) as fh: fh.write(content) fh.flush() @@ -90,36 +87,34 @@ test self.assertIn(code, check.REPORTS) def test_correct_length(self): - conf = { - 'max_line_length': 79, - 'allow_long_titles': True, - } - with tempfile.NamedTemporaryFile(suffix='.rst') as fh: - fh.write(b'known exploit in the wild, for example' - b' \xe2\x80\x93 the time' - b' between advance notification') + conf = {"max_line_length": 79, "allow_long_titles": True} + with tempfile.NamedTemporaryFile(suffix=".rst") as fh: + fh.write( + b"known exploit in the wild, for example" + b" \xe2\x80\x93 the time" + b" between advance notification" + ) fh.flush() - parsed_file = parser.ParsedFile(fh.name, encoding='utf-8') + parsed_file = parser.ParsedFile(fh.name, encoding="utf-8") check = checks.CheckMaxLineLength(conf) errors = list(check.report_iter(parsed_file)) self.assertEqual(0, len(errors)) def test_ignore_code_block(self): - conf = { - 'max_line_length': 79, - 'allow_long_titles': True, - } - with tempfile.NamedTemporaryFile(suffix='.rst') as fh: - fh.write(b'List which contains items with code-block\n' - b'- this is a list item\n\n' - b' .. code-block:: ini\n\n' - b' this line exceeds 80 chars but should be ignored' - b'this line exceeds 80 chars but should be ignored' - b'this line exceeds 80 chars but should be ignored') + conf = {"max_line_length": 79, "allow_long_titles": True} + with tempfile.NamedTemporaryFile(suffix=".rst") as fh: + fh.write( + b"List which contains items with code-block\n" + b"- this is a list item\n\n" + b" .. code-block:: ini\n\n" + b" this line exceeds 80 chars but should be ignored" + b"this line exceeds 80 chars but should be ignored" + b"this line exceeds 80 chars but should be ignored" + ) fh.flush() - parsed_file = parser.ParsedFile(fh.name, encoding='utf-8') + parsed_file = parser.ParsedFile(fh.name, encoding="utf-8") check = checks.CheckMaxLineLength(conf) errors = list(check.report_iter(parsed_file)) self.assertEqual(0, len(errors)) @@ -138,14 +133,11 @@ test content += b"\n\n" content += b"a" * 100 content += b"\n" - conf = { - 'max_line_length': 79, - 'allow_long_titles': True, - } + conf = {"max_line_length": 79, "allow_long_titles": True} # This number is different since rst parsing is aware that titles # are allowed to be over-length, while txt parsing is not aware of # this fact (since it has no concept of title sections). - extensions = [(0, '.rst'), (1, '.txt')] + extensions = [(0, ".rst"), (1, ".txt")] for expected_errors, ext in extensions: with tempfile.NamedTemporaryFile(suffix=ext) as fh: fh.write(content) @@ -157,18 +149,17 @@ test self.assertEqual(expected_errors, len(errors)) def test_definition_term_length(self): - conf = { - 'max_line_length': 79, - 'allow_long_titles': True, - } - with tempfile.NamedTemporaryFile(suffix='.rst') as fh: - fh.write(b'Definition List which contains long term.\n\n' - b'looooooooooooooooooooooooooooooong definition term' - b'this line exceeds 80 chars but should be ignored\n' - b' this is a definition\n') + conf = {"max_line_length": 79, "allow_long_titles": True} + with tempfile.NamedTemporaryFile(suffix=".rst") as fh: + fh.write( + b"Definition List which contains long term.\n\n" + b"looooooooooooooooooooooooooooooong definition term" + b"this line exceeds 80 chars but should be ignored\n" + b" this is a definition\n" + ) fh.flush() - parsed_file = parser.ParsedFile(fh.name, encoding='utf-8') + parsed_file = parser.ParsedFile(fh.name, encoding="utf-8") check = checks.CheckMaxLineLength(conf) errors = list(check.report_iter(parsed_file)) self.assertEqual(0, len(errors)) @@ -176,10 +167,12 @@ test class TestNewlineEndOfFile(testtools.TestCase): def test_newline(self): - tests = [(1, b"testing"), - (1, b"testing\ntesting"), - (0, b"testing\n"), - (0, b"testing\ntesting\n")] + tests = [ + (1, b"testing"), + (1, b"testing\ntesting"), + (0, b"testing\n"), + (0, b"testing\ntesting\n"), + ] for expected_errors, line in tests: with tempfile.NamedTemporaryFile() as fh: diff --git a/doc8/tests/test_main.py b/doc8/tests/test_main.py new file mode 100644 index 0000000000000000000000000000000000000000..c332a7841140f2f68889e6ccef881d02c241cce1 --- /dev/null +++ b/doc8/tests/test_main.py @@ -0,0 +1,428 @@ +# -*- coding: utf-8 -*- + +from mock import patch, MagicMock +import os +import six +import shutil +import sys +import testtools + +from doc8.main import main, doc8 + + +# Location to create test files +TMPFS_DIR_NAME = ".tmp" + +# Expected output +OUTPUT_CMD_NO_QUIET = """\ +Scanning... +Validating... +{path}/invalid.rst:1: D002 Trailing whitespace +{path}/invalid.rst:1: D005 No newline at end of file +======== +Total files scanned = 1 +Total files ignored = 0 +Total accumulated errors = 2 +Detailed error counts: + - doc8.checks.CheckCarriageReturn = 0 + - doc8.checks.CheckIndentationNoTab = 0 + - doc8.checks.CheckMaxLineLength = 0 + - doc8.checks.CheckNewlineEndOfFile = 1 + - doc8.checks.CheckTrailingWhitespace = 1 + - doc8.checks.CheckValidity = 0 +""" + +OUTPUT_CMD_QUIET = """\ +{path}/invalid.rst:1: D002 Trailing whitespace +{path}/invalid.rst:1: D005 No newline at end of file +""" + +OUTPUT_CMD_VERBOSE = """\ +Scanning... + Selecting '{path}/invalid.rst' +Validating... +Validating {path}/invalid.rst (ascii, 10 chars, 1 lines) + Running check 'doc8.checks.CheckValidity' + Running check 'doc8.checks.CheckTrailingWhitespace' + - {path}/invalid.rst:1: D002 Trailing whitespace + Running check 'doc8.checks.CheckIndentationNoTab' + Running check 'doc8.checks.CheckCarriageReturn' + Running check 'doc8.checks.CheckMaxLineLength' + Running check 'doc8.checks.CheckNewlineEndOfFile' + - {path}/invalid.rst:1: D005 No newline at end of file +======== +Total files scanned = 1 +Total files ignored = 0 +Total accumulated errors = 2 +Detailed error counts: + - doc8.checks.CheckCarriageReturn = 0 + - doc8.checks.CheckIndentationNoTab = 0 + - doc8.checks.CheckMaxLineLength = 0 + - doc8.checks.CheckNewlineEndOfFile = 1 + - doc8.checks.CheckTrailingWhitespace = 1 + - doc8.checks.CheckValidity = 0 +""" + +OUTPUT_API_REPORT = """\ +{path}/invalid.rst:1: D002 Trailing whitespace +{path}/invalid.rst:1: D005 No newline at end of file +======== +Total files scanned = 1 +Total files ignored = 0 +Total accumulated errors = 2 +Detailed error counts: + - doc8.checks.CheckCarriageReturn = 0 + - doc8.checks.CheckIndentationNoTab = 0 + - doc8.checks.CheckMaxLineLength = 0 + - doc8.checks.CheckNewlineEndOfFile = 1 + - doc8.checks.CheckTrailingWhitespace = 1 + - doc8.checks.CheckValidity = 0""" + + +class Capture(object): + """ + Context manager to capture output on stdout and stderr + """ + + def __enter__(self): + self.old_out = sys.stdout + self.old_err = sys.stderr + self.out = six.StringIO() + self.err = six.StringIO() + + sys.stdout = self.out + sys.stderr = self.err + + return self.out, self.err + + def __exit__(self, *args): + sys.stdout = self.out + sys.stderr = self.err + + +class TmpFs(object): + """ + Context manager to create and clean a temporary file area for testing + """ + + def __enter__(self): + self.path = os.path.join(os.getcwd(), TMPFS_DIR_NAME) + if os.path.exists(self.path): + raise ValueError( + "Tmp dir found at %s - remove before running tests" % self.path + ) + os.mkdir(self.path) + return self + + def __exit__(self, *args): + shutil.rmtree(self.path) + + def create_file(self, filename, content): + with open(os.path.join(self.path, filename), "w") as file: + file.write(content) + + def mock(self): + """ + Create a file which fails on a LineCheck and a ContentCheck + """ + self.create_file("invalid.rst", "D002 D005 ") + + def expected(self, template): + """ + Insert the path into a template to generate an expected test value + """ + return template.format(path=self.path) + + +class FakeResult(object): + """ + Minimum valid result returned from doc8 + """ + + total_errors = 0 + + def report(self): + return "" + + +class TestCommandLine(testtools.TestCase): + """ + Test command line invocation + """ + + def test_main__no_quiet_no_verbose__output_is_not_quiet(self): + with TmpFs() as tmpfs, Capture() as (out, err), patch( + "argparse._sys.argv", ["doc8", tmpfs.path] + ): + tmpfs.mock() + state = main() + self.assertEqual(out.getvalue(), tmpfs.expected(OUTPUT_CMD_NO_QUIET)) + self.assertEqual(err.getvalue(), "") + self.assertEqual(state, 1) + + def test_main__quiet_no_verbose__output_is_quiet(self): + with TmpFs() as tmpfs, Capture() as (out, err), patch( + "argparse._sys.argv", ["doc8", "--quiet", tmpfs.path] + ): + tmpfs.mock() + state = main() + self.assertEqual(out.getvalue(), tmpfs.expected(OUTPUT_CMD_QUIET)) + self.assertEqual(err.getvalue(), "") + self.assertEqual(state, 1) + + def test_main__no_quiet_verbose__output_is_verbose(self): + with TmpFs() as tmpfs, Capture() as (out, err), patch( + "argparse._sys.argv", ["doc8", "--verbose", tmpfs.path] + ): + tmpfs.mock() + state = main() + self.assertEqual(out.getvalue(), tmpfs.expected(OUTPUT_CMD_VERBOSE)) + self.assertEqual(err.getvalue(), "") + self.assertEqual(state, 1) + + +class TestApi(testtools.TestCase): + """ + Test direct code invocation + """ + + def test_doc8__defaults__no_output_and_report_as_expected(self): + with TmpFs() as tmpfs, Capture() as (out, err): + tmpfs.mock() + result = doc8(paths=[tmpfs.path]) + + self.assertEqual(result.report(), tmpfs.expected(OUTPUT_API_REPORT)) + self.assertEqual( + result.errors, + [ + ( + "doc8.checks.CheckTrailingWhitespace", + "{}/invalid.rst".format(tmpfs.path), + 1, + "D002", + "Trailing whitespace", + ), + ( + "doc8.checks.CheckNewlineEndOfFile", + "{}/invalid.rst".format(tmpfs.path), + 1, + "D005", + "No newline at end of file", + ), + ], + ) + self.assertEqual(out.getvalue(), "") + self.assertEqual(err.getvalue(), "") + self.assertEqual(result.total_errors, 2) + + def test_doc8__verbose__verbose_overridden(self): + with TmpFs() as tmpfs, Capture() as (out, err): + tmpfs.mock() + result = doc8(paths=[tmpfs.path], verbose=True) + + self.assertEqual(result.report(), tmpfs.expected(OUTPUT_API_REPORT)) + self.assertEqual(out.getvalue(), "") + self.assertEqual(err.getvalue(), "") + self.assertEqual(result.total_errors, 2) + + +class TestArguments(testtools.TestCase): + """ + Test that arguments are parsed correctly + """ + + def get_args(self, **kwargs): + # Defaults + args = { + "paths": [os.getcwd()], + "config": [], + "allow_long_titles": False, + "ignore": set(), + "sphinx": True, + "ignore_path": [], + "ignore_path_errors": {}, + "default_extension": "", + "file_encoding": "", + "max_line_length": 79, + "extension": list([".rst", ".txt"]), + "quiet": False, + "verbose": False, + "version": False, + } + args.update(kwargs) + return args + + def test_get_args__override__value_overridden(self): + # Just checking a dict is a dict, but confirms nothing silly has happened + # so we can make assumptions about get_args in other tests + self.assertEqual( + self.get_args(paths=["/tmp/does/not/exist"]), + { + "paths": ["/tmp/does/not/exist"], + "config": [], + "allow_long_titles": False, + "ignore": set(), + "sphinx": True, + "ignore_path": [], + "ignore_path_errors": {}, + "default_extension": "", + "file_encoding": "", + "max_line_length": 79, + "extension": [".rst", ".txt"], + "quiet": False, + "verbose": False, + "version": False, + }, + ) + + def test_args__no_args__defaults(self): + mock_scan = MagicMock(return_value=([], 0)) + with patch("doc8.main.scan", mock_scan), patch("argparse._sys.argv", ["doc8"]): + state = main() + self.assertEqual(state, 0) + mock_scan.assert_called_once_with(self.get_args()) + + def test_args__paths__overrides_default(self): + mock_scan = MagicMock(return_value=([], 0)) + with patch("doc8.main.scan", mock_scan), patch( + "argparse._sys.argv", ["doc8", "path1", "path2"] + ): + state = main() + self.assertEqual(state, 0) + mock_scan.assert_called_once_with(self.get_args(paths=["path1", "path2"])) + + def test_args__config__overrides_default(self): + mock_scan = MagicMock(return_value=([], 0)) + mock_config = MagicMock(return_value={}) + with patch("doc8.main.scan", mock_scan), patch( + "doc8.main.extract_config", mock_config + ), patch( + "argparse._sys.argv", ["doc8", "--config", "path1", "--config", "path2"] + ): + state = main() + self.assertEqual(state, 0) + mock_scan.assert_called_once_with(self.get_args(config=["path1", "path2"])) + + def test_args__allow_long_titles__overrides_default(self): + mock_scan = MagicMock(return_value=([], 0)) + with patch("doc8.main.scan", mock_scan), patch( + "argparse._sys.argv", ["doc8", "--allow-long-titles"] + ): + state = main() + self.assertEqual(state, 0) + mock_scan.assert_called_once_with(self.get_args(allow_long_titles=True)) + + def test_args__ignore__overrides_default(self): + mock_scan = MagicMock(return_value=([], 0)) + with patch("doc8.main.scan", mock_scan), patch( + "argparse._sys.argv", + ["doc8", "--ignore", "D002", "--ignore", "D002", "--ignore", "D005"], + ): + state = main() + self.assertEqual(state, 0) + mock_scan.assert_called_once_with(self.get_args(ignore={"D002", "D005"})) + + def test_args__sphinx__overrides_default(self): + mock_scan = MagicMock(return_value=([], 0)) + with patch("doc8.main.scan", mock_scan), patch( + "argparse._sys.argv", ["doc8", "--no-sphinx"] + ): + state = main() + self.assertEqual(state, 0) + mock_scan.assert_called_once_with(self.get_args(sphinx=False)) + + def test_args__ignore_path__overrides_default(self): + mock_scan = MagicMock(return_value=([], 0)) + with patch("doc8.main.scan", mock_scan), patch( + "argparse._sys.argv", + ["doc8", "--ignore-path", "path1", "--ignore-path", "path2"], + ): + state = main() + self.assertEqual(state, 0) + mock_scan.assert_called_once_with( + self.get_args(ignore_path=["path1", "path2"]) + ) + + def test_args__ignore_path_errors__overrides_default(self): + mock_scan = MagicMock(return_value=([], 0)) + with patch("doc8.main.scan", mock_scan), patch( + "argparse._sys.argv", + [ + "doc8", + "--ignore-path-errors", + "path1;D002", + "--ignore-path-errors", + "path2;D005", + ], + ): + state = main() + self.assertEqual(state, 0) + mock_scan.assert_called_once_with( + self.get_args(ignore_path_errors={"path1": {"D002"}, "path2": {"D005"}}) + ) + + def test_args__default_extension__overrides_default(self): + mock_scan = MagicMock(return_value=([], 0)) + with patch("doc8.main.scan", mock_scan), patch( + "argparse._sys.argv", ["doc8", "--default-extension", "rst"] + ): + state = main() + self.assertEqual(state, 0) + mock_scan.assert_called_once_with(self.get_args(default_extension="rst")) + + def test_args__file_encoding__overrides_default(self): + mock_scan = MagicMock(return_value=([], 0)) + with patch("doc8.main.scan", mock_scan), patch( + "argparse._sys.argv", ["doc8", "--file-encoding", "utf8"] + ): + state = main() + self.assertEqual(state, 0) + mock_scan.assert_called_once_with(self.get_args(file_encoding="utf8")) + + def test_args__max_line_length__overrides_default(self): + mock_scan = MagicMock(return_value=([], 0)) + with patch("doc8.main.scan", mock_scan), patch( + "argparse._sys.argv", ["doc8", "--max-line-length", "88"] + ): + state = main() + self.assertEqual(state, 0) + mock_scan.assert_called_once_with(self.get_args(max_line_length=88)) + + def test_args__extension__overrides_default(self): + # ": [".rst", ".txt"], + mock_scan = MagicMock(return_value=([], 0)) + with patch("doc8.main.scan", mock_scan), patch( + "argparse._sys.argv", ["doc8", "--extension", "ext1", "--extension", "ext2"] + ): + state = main() + self.assertEqual(state, 0) + mock_scan.assert_called_once_with( + self.get_args(extension=[".rst", ".txt", "ext1", "ext2"]) + ) + + def test_args__quiet__overrides_default(self): + mock_scan = MagicMock(return_value=([], 0)) + with patch("doc8.main.scan", mock_scan), patch( + "argparse._sys.argv", ["doc8", "--quiet"] + ): + state = main() + self.assertEqual(state, 0) + mock_scan.assert_called_once_with(self.get_args(quiet=True)) + + def test_args__verbose__overrides_default(self): + mock_scan = MagicMock(return_value=([], 0)) + with patch("doc8.main.scan", mock_scan), patch( + "argparse._sys.argv", ["doc8", "--verbose"] + ): + state = main() + self.assertEqual(state, 0) + mock_scan.assert_called_once_with(self.get_args(verbose=True)) + + def test_args__version__overrides_default(self): + mock_scan = MagicMock(return_value=([], 0)) + with patch("doc8.main.scan", mock_scan), patch( + "argparse._sys.argv", ["doc8", "--version"] + ): + state = main() + self.assertEqual(state, 0) + mock_scan.assert_not_called() diff --git a/doc8/utils.py b/doc8/utils.py index d500d1527607eaa5c97827260f096b228b65f665..5f5af09959974e1af4a16088a0b5478148311a3b 100644 --- a/doc8/utils.py +++ b/doc8/utils.py @@ -1,7 +1,5 @@ # Copyright (C) 2014 Ivan Melnikov # -# Author: Joshua Harlow -# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -55,7 +53,7 @@ def find_files(paths, extensions, ignored_paths): if extension_matches(path): yield (path, path_ignorable(path)) else: - raise IOError('Invalid path: %s' % path) + raise IOError("Invalid path: %s" % path) def filtered_traverse(document, filter_func): diff --git a/doc8/version.py b/doc8/version.py index 6e33c884f9f4e6a8c7900277f34d555ce1418e23..5ceb9fab7a7c6718c151a69df7b12cc9e64dfd47 100644 --- a/doc8/version.py +++ b/doc8/version.py @@ -1,24 +1,26 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved. +# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. try: from pbr import version as pbr_version - _version_info = pbr_version.VersionInfo('doc8') - version_string = _version_info.version_string + + _version_info = pbr_version.VersionInfo("doc8") + version_string = _version_info.version_string() except ImportError: import pkg_resources - _version_info = pkg_resources.get_distribution('doc8') - version_string = lambda: _version_info.version + + _version_info = pkg_resources.get_distribution("doc8") + version_string = _version_info.version diff --git a/pylintrc b/pylintrc deleted file mode 100644 index 4ec63b1d3bfd27bce05e0e1800f8c65dbbae3eaa..0000000000000000000000000000000000000000 --- a/pylintrc +++ /dev/null @@ -1,31 +0,0 @@ -# The format of this file isn't really documented; just use --generate-rcfile - -[Messages Control] -# C0111: Don't require docstrings on every method -# W0511: TODOs in code comments are fine. -# W0142: *args and **kwargs are fine. -# W0622: Redefining id is fine. -disable=C0111,W0511,W0142,W0622 - -[Basic] -# Variable names can be 1 to 31 characters long, with lowercase and underscores -variable-rgx=[a-z_][a-z0-9_]{0,30}$ - -# Argument names can be 2 to 31 characters long, with lowercase and underscores -argument-rgx=[a-z_][a-z0-9_]{1,30}$ - -# Method names should be at least 3 characters long -# and be lowercased with underscores -method-rgx=([a-z_][a-z0-9_]{2,50}|setUp|tearDown)$ - -[Design] -max-public-methods=100 -min-public-methods=0 -max-args=6 - -[Variables] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -# _ is used by our localization -additional-builtins=_ diff --git a/requirements.txt b/requirements.txt index 8fcc51273afe9cfb17b1187312f2b715c27b719a..7b73c720159596902dd1be55fa87d33e88bcccbc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ docutils restructuredtext-lint>=0.7 six stevedore +Pygments diff --git a/setup.cfg b/setup.cfg index 0f9b7a7df65e5c53266e858dbb2a24940fff297f..b8dad36d973d07f39ecfdb7d51046b95555b3f5d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,8 +4,11 @@ summary = Style checker for Sphinx (or other) RST documentation description-file = README.rst author = OpenStack -author-email = openstack-dev@lists.openstack.org -home-page = https://launchpad.net/doc8 +author_email = openstack-discuss@lists.openstack.org +maintainer = PyCQA +maintainer_email = code-quality@python.org +home-page = https://github.com/pycqa/doc8 +long_description_content_type = text/x-rst classifier = Intended Audience :: Information Technology Intended Audience :: System Administrators @@ -18,16 +21,22 @@ classifier = Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 [entry_points] console_scripts = doc8 = doc8.main:main -[build_sphinx] -all_files = 1 -build-dir = doc/build -source-dir = doc/source - [wheel] universal = 1 + +[flake8] +builtins = _ +show-source = True +exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build +# Minimal config needed to make flake8 compatible with black output: +max-line-length=160 +# See https://github.com/PyCQA/pycodestyle/issues/373 +extend-ignore = E203 diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 736375744d94f6f122f98fab48b8fc77b13d8bec..90623e289751056f419c4adb61809bf068e8365b --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +24,4 @@ try: except ImportError: pass -setuptools.setup( - setup_requires=['pbr'], - pbr=True) +setuptools.setup(setup_requires=["pbr"], pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt index 0b9685f66504a1ead46df9870bfbd172bc53cd5b..3df1aef442f2959366d88894bfe2dc2a12ac82c1 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,10 +1,3 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. - -doc8 -hacking>=0.9.2,<0.10 -nose -oslosphinx -sphinx>=1.1.2,!=1.2.0,<1.3 -testtools +mock # BSD +nose # LGPL +testtools # MIT diff --git a/tox.ini b/tox.ini index 864ea24233d2d649d393ef5f8aac7e8d276dad54..a345b00f09f2f30d7f34db839e3d89c0f8c65a50 100644 --- a/tox.ini +++ b/tox.ini @@ -1,32 +1,43 @@ [tox] -minversion = 1.6 -skipsdist = True -envlist = py35,py27,pep8 +minversion = 3.8 +envlist = lint,py{27,35,36,37},docs,packaging [testenv] -setenv = VIRTUAL_ENV={envdir} -usedevelop = True -install_command = pip install {opts} {packages} -deps = -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt +deps = + -r{toxinidir}/test-requirements.txt commands = nosetests {posargs} +whitelist_externals = + rm -[testenv:pep8] -commands = flake8 {posargs} - -[testenv:pylint] -requirements = pylint==0.25.2 -commands = pylint doc8 - -[testenv:venv] -commands = {posargs} +[testenv:lint] +deps = + pre-commit +commands = + pre-commit run -a [testenv:docs] +deps = + -r{toxinidir}/doc/requirements.txt commands = - doc8 -e .rst doc CONTRIBUTING.rst HACKING.rst README.rst - python setup.py build_sphinx + doc8 -e .rst doc CONTRIBUTING.rst README.rst + sphinx-build -W -b html doc/source doc/build/html -[flake8] -builtins = _ -show-source = True -exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build +[testenv:packaging] +description = + Do packagin/distribution. If tag is not present or PEP440 compliant upload to + PYPI could fail +# `usedevelop = true` overrides `skip_install` instruction, it's unwanted +usedevelop = false +# don't install molecule itself in this env +skip_install = true +deps = + collective.checkdocs >= 0.2 + pep517 >= 0.5.0 + twine >= 1.14.0 +setenv = +commands = + rm -rfv {toxinidir}/dist/ + python setup.py sdist bdist_wheel + # metadata validation + python -m setup checkdocs --verbose + python -m twine check {toxinidir}/dist/*