diff --git a/.github/ISSUE_TEMPLATE/bug-report-lib.yaml b/.github/ISSUE_TEMPLATE/bug-report-lib.yaml new file mode 100644 index 00000000..a35e8d42 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report-lib.yaml @@ -0,0 +1,125 @@ +--- +name: "\U0001F41E Bug Report (rstcheck as a library)" +description: Found a bug when using rstcheck as a library? Submit them here! +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + First of all thank you for discovering and submitting an issue. + + To increase the chance that someone may be able to help you + please fill the form below as good as you can. + + - type: checkboxes + id: todos + attributes: + label: To Dos + description: Before submitting please confirm and check the following options. + options: + - label: > + I tested with the + [latest released version](https://github.com/rstcheck/rstcheck/releases/latest). + required: true + - label: > + I have checked the [issues](https://github.com/rstcheck/rstcheck/issues) + and think that this is not a duplicate. + required: true + - label: I added a very descriptive title to this issue. + required: true + + - type: textarea + id: example + attributes: + label: Example Code (python) + description: > + Please add a self-contained, minimal, reproducible example with you use case. + [Help manual](https://stackoverflow.com/help/minimal-reproducible-example) + + If the code can be copied, checked with rstcheck and the issue is directly + reproducible it increases the chance that someone might be able to help. + render: Python + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Relevant log output + description: | + Please copy and paste any relevant log output. + This will be automatically formatted into code, so no need for backticks. + render: Shell + validations: + required: true + + - type: textarea + id: description + attributes: + label: Description + description: | + What is the problem? What did you do? + Write a short description of what you did, what you expected and what accutally happened. + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating System + description: What operating system are you on? + multiple: true + options: + - Linux + - Windows + - macOS + - Other + validations: + required: true + + - type: textarea + id: os-details + attributes: + label: Operating System Details + description: > + Optionally add more information about your operating system, + especially if you choose "Other". + + - type: input + id: python-version + attributes: + label: Python Version + description: | + What Python version(s) are you using? + + You can find the Python version with: + + ```shell + python --version + ``` + validations: + required: true + + - type: input + id: rstcheck-version + attributes: + label: rstcheck Version + description: | + What rstcheck & rstcheck-core versions are you using? + + You can find the versions with: + + ```shell + rstcheck --version + ``` + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: > + Any additional information that did not match the fields above, + but may help to further understand the issue. diff --git a/.github/ISSUE_TEMPLATE/bug-report-tool.yaml b/.github/ISSUE_TEMPLATE/bug-report-tool.yaml new file mode 100644 index 00000000..58f60c94 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report-tool.yaml @@ -0,0 +1,125 @@ +--- +name: "\U0001F41E Bug Report (rstcheck as a tool)" +description: Found a bug when using rstcheck as a tool? Submit them here! +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + First of all thank you for discovering and submitting an issue. + + To increase the chance that someone may be able to help you + please fill the form below as good as you can. + + - type: checkboxes + id: todos + attributes: + label: To Dos + description: Before submitting please confirm and check the following options. + options: + - label: > + I tested with the + [latest released version](https://github.com/rstcheck/rstcheck/releases/latest). + required: true + - label: > + I have checked the [issues](https://github.com/rstcheck/rstcheck/issues) + and think that this is not a duplicate. + required: true + - label: I added a very descriptive title to this issue. + required: true + + - type: textarea + id: example + attributes: + label: Example Code (rst) + description: > + Please add a self-contained, minimal, reproducible example with you use case. + [Help manual](https://stackoverflow.com/help/minimal-reproducible-example) + + If the rst document can be copied, checked with rstcheck and the issue is directly + reproducible it increases the chance that someone might be able to help. + render: rst + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Relevant log output + description: | + Please copy and paste any relevant log output. + This will be automatically formatted into code, so no need for backticks. + render: Shell + validations: + required: true + + - type: textarea + id: description + attributes: + label: Description + description: | + What is the problem? What did you do? + Write a short description of what you did, what you expected and what accutally happened. + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating System + description: What operating system are you on? + multiple: true + options: + - Linux + - Windows + - macOS + - Other + validations: + required: true + + - type: textarea + id: os-details + attributes: + label: Operating System Details + description: > + Optionally add more information about your operating system, + especially if you choose "Other". + + - type: input + id: python-version + attributes: + label: Python Version + description: | + What Python version(s) are you using? + + You can find the Python version with: + + ```shell + python --version + ``` + validations: + required: true + + - type: input + id: rstcheck-version + attributes: + label: rstcheck Version + description: | + What rstcheck & rstcheck-core versions are you using? + + You can find the versions with: + + ```shell + rstcheck --version + ``` + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: > + Any additional information that did not match the fields above, + but may help to further understand the issue. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..bd9dfe4e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,2 @@ +--- +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature-request.yaml b/.github/ISSUE_TEMPLATE/feature-request.yaml new file mode 100644 index 00000000..23b7f4ac --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yaml @@ -0,0 +1,90 @@ +--- +name: "\U0001F4A1 Feature Request" +description: Ideas for new features and/or improvements? Submit them here! +title: "[Feature]: " +labels: ["feature", "triage"] + +body: + - type: markdown + attributes: + value: | + First of all thank you for discovering and submitting an issue. + + To increase the chance that someone may be able to help you + please fill the form below as good as you can. + + - type: checkboxes + id: todos + attributes: + label: To Dos + description: Before submitting please confirm and check the following options. + options: + - label: > + I have checked the [issues](https://github.com/rstcheck/rstcheck/issues) + and think that this is not a duplicate. + required: true + - label: I added a very descriptive title to this issue. + required: true + + - type: textarea + id: description + attributes: + label: Description + description: | + What feature do you want to see beeing added? + Why do you want the feature? What are you circumstances? + How do you imagine that could be accomplished? + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating System + description: What operating system are you on? + multiple: true + options: + - Linux + - Windows + - macOS + - Other + validations: + required: true + + - type: textarea + id: os-details + attributes: + label: Operating System Details + description: > + Optionally add more information about your operating system, + especially if you choose "Other". + + - type: input + id: python-version + attributes: + label: Python Version + description: | + What Python version(s) are you using? + + You can find the Python version with: + + ```shell + python --version + ``` + validations: + required: true + + - type: input + id: rstcheck-version + attributes: + label: rstcheck Version + description: | + What rstcheck & rstcheck-core versions are you using? + + You can find the versions with: + + ```shell + rstcheck --version + ``` + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/question.yaml b/.github/ISSUE_TEMPLATE/question.yaml new file mode 100644 index 00000000..a7e218c1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yaml @@ -0,0 +1,91 @@ +--- +name: "¯\\_(ツ)_/¯ Something Else" +description: You simply have a question or something else? Submit them here! +title: "[Question]: " +labels: ["question", "triage"] + +body: + - type: markdown + attributes: + value: | + First of all thank you for discovering and submitting an issue. + + To increase the chance that someone may be able to help you + please fill the form below as good as you can. + + - type: checkboxes + id: todos + attributes: + label: To Dos + description: Before submitting please confirm and check the following options. + options: + - label: I already searched with my favorite search engine "How to do X with rstcheck". + required: true + - label: I already read and followed the README and didn't find an answer. + required: true + - label: > + I have checked the [issues](https://github.com/rstcheck/rstcheck/issues) + and think that this is not a duplicate. + required: true + - label: I added a very descriptive title to this issue. + required: true + + - type: textarea + id: description + attributes: + label: Description + description: Describe your question or issue. + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating System + description: What operating system are you on? + multiple: true + options: + - Linux + - Windows + - macOS + - Other + validations: + required: true + + - type: textarea + id: os-details + attributes: + label: Operating System Details + description: > + Optionally add more information about your operating system, + especially if you choose "Other". + + - type: input + id: python-version + attributes: + label: Python Version + description: | + What Python version(s) are you using? + + You can find the Python version with: + + ```shell + python --version + ``` + validations: + required: true + + - type: input + id: rstcheck-version + attributes: + label: rstcheck Version + description: | + What rstcheck & rstcheck-core versions are you using? + + You can find the versions with: + + ```shell + rstcheck --version + ``` + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..5a435bef --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ + + + +# Check List + +Resolves: # + +- [ ] I added **tests** for the changed code. +- [ ] I updated the **documentation** for the changed code. +- [ ] I ran the **full** `tox` test suite locally, so the CI pipelines should be green. +- [ ] I added the change to the CHANGELOG.md file. + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..2db14896 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +--- +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..f381e963 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,65 @@ +--- +name: "CodeQL" + +on: # yamllint disable-line rule:truthy + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "34 3 * * 2" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["python"] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # yamllint disable-line rule:line-length + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # yamllint disable-line rule:line-length + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE + # below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 00000000..5d0a1c58 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,57 @@ +--- +name: Test documentation + +env: + CI_FORCE_COLORS_SPHINX: --color + +on: # yamllint disable-line rule:truthy + workflow_dispatch: + + push: + tags: + - "!*" + branches: + - main + - "test-me-*" + + pull_request: + branches: + - "**" + +jobs: + build: + name: Tests on ${{ matrix.os }} with default python + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest", "windows-latest", "macos-latest"] + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Display Python version + run: python --version + + - name: Upgrade pip + run: python -m pip install --upgrade pip + + - name: Install pipx + run: python -m pip install --upgrade pipx + + - name: Install tox via pipx + run: pipx install tox + + - name: Run tests with tox except linkcheck and spelling + if: runner.os != 'Linux' + run: tox -m docs + + - name: Run all tests with tox + if: runner.os == 'Linux' + run: tox -m docs-full diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml new file mode 100644 index 00000000..5556ff4c --- /dev/null +++ b/.github/workflows/qa.yml @@ -0,0 +1,46 @@ +--- +name: QA + +env: + CI_FORCE_COLORS_PRE_COMMIT: --color always + +on: # yamllint disable-line rule:truthy + workflow_dispatch: + + push: + tags: + - "!*" + branches: + - main + - "test-me-*" + + pull_request: + branches: + - "**" + +jobs: + build: + name: Run QA Tools + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Display Python version + run: python --version + + - name: Upgrade pip + run: python -m pip install --upgrade pip + + - name: Install pipx + run: python -m pip install --upgrade pipx + + - name: Install tox via pipx + run: pipx install tox + + - name: Run pre-commit via tox + run: tox -e pre-commit-run diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..cb914ede --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,66 @@ +--- +name: Test code + +env: + CI_FORCE_COLORS_PYTEST: --color yes + +on: # yamllint disable-line rule:truthy + workflow_dispatch: + + push: + tags: + - "!*" + branches: + - main + - "test-me-*" + + pull_request: + branches: + - "**" + +jobs: + build: + name: Tests on ${{ matrix.os }} with python ${{ matrix.python-version }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest", "windows-latest", "macos-latest"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v3 + + - name: Fetch origin/main + #: https://github.com/Bachmann1234/diff_cover#troubleshooting + run: git fetch --no-tags origin main:refs/remotes/origin/main + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Display Python version + run: python --version + + - name: Upgrade pip + run: python -m pip install --upgrade pip + + - name: Install pipx + run: python -m pip install --upgrade pipx + + - name: Install tox via pipx + run: pipx install tox + + - name: Run tests via tox (linux/macos) + if: runner.os != 'Windows' + run: | + major=$(python -c "import sys;print(sys.version_info[0])") + minor=$(python -c "import sys;print(sys.version_info[1])") + tox -m py${major}.${minor} + + - name: Run tests via tox (windows) + if: runner.os == 'Windows' + run: | + $major = python -c "import sys;print(sys.version_info[0])" + $minor = python -c "import sys;print(sys.version_info[1])" + tox -m py${major}.${minor} diff --git a/.github/workflows/update-authors.yaml b/.github/workflows/update-authors.yaml new file mode 100644 index 00000000..8eb8aba8 --- /dev/null +++ b/.github/workflows/update-authors.yaml @@ -0,0 +1,90 @@ +--- +name: Update AUTHORS.rst + +# What this workflow does: +# 1. Update the AUTHORS.rst file +# 2. Git commit and push the file if there are changes. + +on: # yamllint disable-line rule:truthy + workflow_dispatch: + + push: + tags: + - "!*" + branches: + - main + - "test-me-*" + +jobs: + update-authors: + name: Update AUTHORS.rst file + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Update AUTHORS.rst file + shell: python + run: | + import subprocess + + git_authors = subprocess.run( + ["git", "log", "--format=%aN <%aE>"], capture_output=True, check=True + ).stdout.decode() + + skip_list = ( + "Steven Myint", + "dependabot", + "pre-commit-ci", + "github-action", + "GitHub Actions", + ) + authors = [ + author + for author in set(git_authors.strip().split("\n")) + if not author.startswith(skip_list) + ] + authors.sort() + + file_head = ( + ".. This file is automatically generated/updated by a github actions workflow.\n" + ".. Every manual change will be overwritten on push to main.\n" + ".. You can find it here: ``.github/workflows/update-authors.yaml``\n" + ".. For more information see " + "`https://github.com/rstcheck/rstcheck/graphs/contributors`\n\n" + "Author\n" + "------\n" + "Steven Myint \n\n" + "Additional contributions by (sorted by name)\n" + "--------------------------------------------\n" + ) + + with open("AUTHORS.rst", "w") as authors_file: + authors_file.write(file_head) + authors_file.write("- ") + authors_file.write("\n- ".join(authors)) + authors_file.write("\n") + + - name: Check if diff + continue-on-error: true + run: > + git diff --exit-code AUTHORS.rst && + (echo "### No update" && exit 1) || (echo "### Commit update") + + - uses: EndBug/add-and-commit@v9 + name: Commit and push if diff + if: success() + with: + add: AUTHORS.rst + message: Update AUTHORS.rst file with new author(s) + author_name: GitHub Actions + author_email: action@github.com + committer_name: GitHub Actions + committer_email: actions@github.com + push: true diff --git a/.gitignore b/.gitignore index 67e2e25b..022e03c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .*.swp .eggs/ +.tox/ dist build *.pyc @@ -11,3 +12,8 @@ build *.nav *.out *.snm +*.bak.* +objects.*.txt +.coverage_cache/ +.flakeheaven_cache/ +__version__.py diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 00000000..936d01ce --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,35 @@ +// Rules: https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md +{ + "config": { + "MD003": "atx", // heading-style + "MD004": { + // ul-style + "style": "dash", + }, + "MD013": { + // line-length + "line_length": 100, + }, + "MD014": false, // commands-show-output + "MD024": { + // no-duplicate-heading + "siblings_only": true, + }, + "MD035": { + // hr-style + "style": "---", + }, + "MD048": { + // code-fence-style + "style": "backtick", + }, + "MD049": { + // emphasis-style + "style": "underscore", + }, + "MD050": { + // strong-style + "style": "asterisk", + }, + }, +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..229c0fe0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,249 @@ +--- +minimum_pre_commit_version: "2.17" +default_stages: [commit] +default_language_version: + python: python3.12 + +ci: + skip: [mypy, pylint] + +repos: + # ---------------------------------------------- + # Meta hooks + # ---------------------------------------------- + + - repo: meta + hooks: + - id: identity + stages: [commit, manual] + - id: check-hooks-apply + stages: [manual] + - id: check-useless-excludes + stages: [manual] + + # ---------------------------------------------- + # File hooks + # ---------------------------------------------- + + # file checking out-of-the-box hooks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: 2c9f875913ee60ca25ce70243dc24d5b6415598c # frozen: v4.6.0 + hooks: + - id: check-case-conflict + - id: check-shebang-scripts-are-executable + exclude: '^.*\.rs$' + stages: [commit] + - id: check-symlinks + - id: destroyed-symlinks + - id: forbid-new-submodules + + # #################################################################################### + # + # FORMATTING + # + # #################################################################################### + + # ---------------------------------------------- + # Python + # ---------------------------------------------- + + # ruff - python linter with fixing ability + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: 1dc9eb131c2ea4816c708e4d85820d2cc8542683 # frozen: v0.5.0 + hooks: + - id: ruff + name: ruff (fix) + args: ["--fix-only", "--exit-non-zero-on-fix", "--config=pyproject.toml"] + - id: ruff-format + name: ruff (format) + + # blacken-docs - black for python code in docs (rst/md/tex) + - repo: https://github.com/asottile/blacken-docs + rev: 4c97c4a0d921007af6fefae92d8447cfbf63720b # frozen: 1.18.0 + hooks: + - id: blacken-docs + args: ["--line-length=100", "--target-version=py312"] + exclude: testing|tests + + # ---------------------------------------------- + # JS / TS / HTML / CSS / MD / JSON / YAML + # ---------------------------------------------- + + # prettier - multi formatter + - repo: https://github.com/pre-commit/mirrors-prettier + rev: f12edd9c7be1c20cfa42420fd0e6df71e42b51ea # frozen: v4.0.0-alpha.8 + hooks: + - id: prettier + additional_dependencies: + - "prettier@^3.2.4" + + # ---------------------------------------------- + # Spelling dict + # ---------------------------------------------- + + # Custom hook as python command + - repo: local + hooks: + - id: sort-spelling-dicts + name: Sort spelling_dict.txt files + description: Sort spelling_dict.txt files + language: python + entry: python + args: + - "-c" + - | + import pathlib; + import sys; + p = pathlib.Path(sys.argv[1]); + p.write_text("\n".join(sorted(set(p.read_text("utf-8").splitlines()))) + "\n", "utf-8") + files: "spelling_dict.txt" + + # ---------------------------------------------- + # General (code unspecific) + # ---------------------------------------------- + + # code unspecific out-of-the-box hooks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: 2c9f875913ee60ca25ce70243dc24d5b6415598c # frozen: v4.6.0 + hooks: + - id: end-of-file-fixer + stages: [commit] + - id: trailing-whitespace + stages: [commit] + + # #################################################################################### + # + # LINTING + # + # #################################################################################### + + # ---------------------------------------------- + # General (code unspecific) + # ---------------------------------------------- + + - repo: local + hooks: + # Find TODO:|FIXME: comments in all files + # Inline skip: `#i#` directly after the colon after the tag-word + - id: find-todos + name: "Find TODO:|FIXME: comments" + description: "Check for TODO:|FIXME: comments in all files" + language: pygrep + entry: '(^|//!?|#| + +## Unreleased + +[diff v6.2.4...main](https://github.com/rstcheck/rstcheck/compare/v6.2.4...main) + +## [v6.2.4 (2024-07-07)](https://github.com/rstcheck/rstcheck/releases/v6.2.4) + +[diff v6.2.3...v6.2.4](https://github.com/rstcheck/rstcheck/compare/v6.2.3...v6.2.4) + +### Documentation + +- Add note on how to disable pretty exception output ([#228](https://github.com/rstcheck/rstcheck/pull/228)) + +### Miscellaneous + +- Add help text to `--version` flag ([#228](https://github.com/rstcheck/rstcheck/pull/228)) + +## [v6.2.3 (2024-07-07)](https://github.com/rstcheck/rstcheck/releases/v6.2.3) + +[diff v6.2.2...v6.2.3](https://github.com/rstcheck/rstcheck/compare/v6.2.2...v6.2.3) + +### Bugfixes + +- Fix typer dependency by removing the `[standard]` extra which is only used on typer-slim. + Typer by default has the extras included. + +## [v6.2.2 (2024-07-07)](https://github.com/rstcheck/rstcheck/releases/v6.2.2) + +[diff v6.2.1...v6.2.2](https://github.com/rstcheck/rstcheck/compare/v6.2.1...v6.2.2) + +### Miscellaneous + +- Bump min. version of typer and fix dependency group name ([#223](https://github.com/rstcheck/rstcheck/issues/223)) +- Update configs for dev tooling ([#225](https://github.com/rstcheck/rstcheck/pull/225)) +- Bump default python version to 3.12 ([#225](https://github.com/rstcheck/rstcheck/pull/225)) + +## [v6.2.1 (2024-03-23)](https://github.com/rstcheck/rstcheck/releases/v6.2.1) + +[diff v6.2.0...v6.2.1](https://github.com/rstcheck/rstcheck/compare/v6.2.0...v6.2.1) + +### Miscellaneous + +- Remove unused pre python 3.8 compatibility code ([#195](https://github.com/rstcheck/rstcheck/pull/195)) +- Drop support for sphinx v4 ([#207](https://github.com/rstcheck/rstcheck/pull/207)) +- Added `__main__.py` to enable command-line execution via python -m rstcheck ([#206](https://github.com/rstcheck/rstcheck/pull/206)) + +## [v6.2.0 (2023-09-09)](https://github.com/rstcheck/rstcheck/releases/v6.2.0) + +[diff v6.1.2...v6.2.0](https://github.com/rstcheck/rstcheck/compare/v6.1.2...v6.2.0) + +### Bugfixes + +- Fix bug where variable in log string was not substituted ([#188](https://github.com/rstcheck/rstcheck/pull/188)) + +### Miscellaneous + +- Update Sphinx Theme Version and remove outdated Dark Mode Lib ([#176](https://github.com/rstcheck/rstcheck/pull/176)) +- Drop support for Sphinx v2 and v3 ([#176](https://github.com/rstcheck/rstcheck/pull/176)) +- Add tox environments for v6 and v7 ([#176](https://github.com/rstcheck/rstcheck/pull/176)) +- Switch from poetry to setuptools ([#187](https://github.com/rstcheck/rstcheck/pull/187)) +- Change test file naming convention ([#188](https://github.com/rstcheck/rstcheck/pull/188)) +- Change dev tooling ([#188](https://github.com/rstcheck/rstcheck/pull/188)) +- Add python 3.12 to CI ([#188](https://github.com/rstcheck/rstcheck/pull/188)) + +## [v6.1.2 (2023-03-12)](https://github.com/rstcheck/rstcheck/releases/v6.1.2) + +[diff v6.1.1...v6.1.2](https://github.com/rstcheck/rstcheck/compare/v6.1.1...v6.1.2) + +### Miscellaneous + +- Update GHA workflows to use latest 'setup-python' action ([#150](https://github.com/rstcheck/rstcheck/issues/150)) +- Set tomli extra dependency to python < 3.11 like rstcheck-core ([#162](https://github.com/rstcheck/rstcheck/issues/162)) +- Drop python 3.7 ([#177](https://github.com/rstcheck/rstcheck/pull/177)) + +## [v6.1.1 (2022-11-12)](https://github.com/rstcheck/rstcheck/releases/v6.1.1) + +[diff v6.1.0...v6.1.1](https://github.com/rstcheck/rstcheck/compare/v6.1.0...v6.1.1) + +### Documentation + +- Add link to rstcheck-core for FAQ +- Remove unused pydantic related stuff from docs ([#149](https://github.com/rstcheck/rstcheck/pull/149)) + +### Miscellaneous + +- Remove unused dependencies (docutils & its stubs, pydantic) ([#149](https://github.com/rstcheck/rstcheck/pull/149)) +- Add python 3.11 to CI + +## [v6.1.0 (2022-08-14)](https://github.com/rstcheck/rstcheck/releases/v6.1.0) + +[diff v6.0.0.post1...v6.1.0](https://github.com/rstcheck/rstcheck/compare/v6.0.0.post1...v6.1.0) + +### Documentation + +- Add note for incompatibility of typer <0.4.1 and click >=8.1 ([#138](https://github.com/rstcheck/rstcheck/issues/138)) +- Update GitHub URL in installation instructions ([#139](https://github.com/rstcheck/rstcheck/pull/139)) +- Fix broken mega-linter URLs ([#136](https://github.com/rstcheck/rstcheck/pull/136)) +- Update release docs for changed release script + +### Miscellaneous + +- Fix release script's changelog insertion +- Add pre-commit-ci badge to README +- Update development tooling dependencies +- Bump lower version constraint on typer from 0.3.2 to 0.4.1 ([#138](https://github.com/rstcheck/rstcheck/issues/138)) + +## [v6.0.0.post1 (2022-06-05)](https://github.com/rstcheck/rstcheck/releases/v6.0.0.post1) + +[diff v6.0.0...v6.0.0.post1](https://github.com/rstcheck/rstcheck/compare/v6.0.0...v6.0.0.post1) + +### Miscellaneous + +- Move release date into version headline link +- Don't include failing test example in sdist ([#128](https://github.com/rstcheck/rstcheck/issues/128)) + +## [v6.0.0 (2022-06-04)](https://github.com/rstcheck/rstcheck/releases/v6.0.0) + +[diff v6.0.0rc3...v6.0.0](https://github.com/rstcheck/rstcheck/compare/v6.0.0rc3...v6.0.0) + +### New features + +- Add a `--version` flag back. This flag gets its information from the metadata in the virtualenv. + +### Documentation + +- Finalize v6 migration guide. +- Add notice to fix `rstcheck-core` version for needed features. + +### Miscellaneous + +- Add tox envs to test with sphinx v5. +- Update `sphinx` `extlinks` config for v5. +- Bump min version of `rstcheck-core` to v1.0.2. + +## [v6.0.0rc3 (2022-05-28)](https://pypi.org/project/rstcheck/6.0.0rc3/) + +[diff v6.0.0rc2...v6.0.0rc3](https://github.com/rstcheck/rstcheck/compare/v6.0.0rc2...v6.0.0rc3) + +### BREAKING CHANGES + +- **MOVED THE CORE LIBRARY INTO IT'S OWN REPOSITORY AT rstcheck/rstcheck-core** +- `rstcheck.config.load_config_file_from_path` now raises an FileNotFoundError if the given path + is neither a file nor a directory ([#125](https://github.com/rstcheck/rstcheck/pull/125)) +- The CLI runner exits 1 when the config path passed with `--config` does not exist ([#125](https://github.com/rstcheck/rstcheck/pull/125)) + +### New features + +- Add `NONE` as a special config file path, to disable config file loading ([#125](https://github.com/rstcheck/rstcheck/pull/125)) + +### Documentation + +- Update config documentation ([#126](https://github.com/rstcheck/rstcheck/pull/126)) + +## [v6.0.0rc2 (2022-05-26)](https://pypi.org/project/rstcheck/6.0.0rc2/) + +[diff v6.0.0rc1...v6.0.0rc2](https://github.com/rstcheck/rstcheck/compare/v6.0.0rc1...v6.0.0rc2) + +### New features + +- Catch SyntaxWarnings in python code-blocks and handle them like SyntaxErrors ([#124](https://github.com/rstcheck/rstcheck/pull/124)) +- Add additional inline configuration and flow control options ([#123](https://github.com/rstcheck/rstcheck/pull/123)) + (see the config docs for more information) + +### Documentation + +- Update links to new repository home at rstcheck/rstcheck +- Update config documentation + +### Miscellaneous + +- Fix release date in changelog for v6.0.0rc1 release +- Set the rstcheck pre-commit hook to run in serial to avoid overhead of doubling parallel runs + with pre-commit +- Little improvements to logging messages +- Rename master branch to main + +## [v6.0.0rc1 (2022-05-21)](https://pypi.org/project/rstcheck/6.0.0rc1/) + +[diff v6.0.0a2...v6.0.0rc1](https://github.com/rstcheck/rstcheck/compare/v6.0.0a2...v6.0.0rc1) + +### BREAKING CHANGES + +- `find_ignored_languages` no longer throws exception but logs warning ([#108](https://github.com/rstcheck/rstcheck/pull/108)) + +### New features + +- Add more thorough documentation ([#112](https://github.com/rstcheck/rstcheck/pull/112)) +- Add `--log-level` option to CLI ([#108](https://github.com/rstcheck/rstcheck/pull/108)) +- Add `--warn-unknown-settings` flag to CLI ([#118](https://github.com/rstcheck/rstcheck/pull/118)) +- Setup logging to console for CLI ([#108](https://github.com/rstcheck/rstcheck/pull/108)) +- Setup logging to console for library (deactivated by default) ([#108](https://github.com/rstcheck/rstcheck/pull/108)) + +## [v6.0.0a2 (2022-05-20)](https://pypi.org/project/rstcheck/6.0.0a2/) + +[diff v6.0.0a1...v6.0.0a2](https://github.com/rstcheck/rstcheck/compare/v6.0.0a1...v6.0.0a2) + +### BREAKING CHANGES + +- String lists for `ignore_*` configs are white-space cleaned at string start and end. + Restores behavior of pre v6. ([#116](https://github.com/rstcheck/rstcheck/pull/116)) + +### New features + +- Add support for INI multi-line string back ([#116](https://github.com/rstcheck/rstcheck/pull/116)) + +### Bugfixes + +- Fix bug #113 - sphinx print warnings for overwriting registered nodes ([#117](https://github.com/rstcheck/rstcheck/pull/117)) + +## [v6.0.0a1 (2022-05-13)](https://pypi.org/project/rstcheck/6.0.0a1/) + +[diff v5.0.0...v6.0.0a1](https://github.com/rstcheck/rstcheck/compare/v5.0.0...v6.0.0a1) + +### BREAKING CHANGES + +- Full restructuring of the code base ([#100](https://github.com/rstcheck/rstcheck/pull/100)) +- Rewrite of CLI with `typer` ([#100](https://github.com/rstcheck/rstcheck/pull/100)) +- Renamed config `report` to `report_level` ([#100](https://github.com/rstcheck/rstcheck/pull/100)) +- Renamed config `ignore_language` to `ignore_languages` ([#100](https://github.com/rstcheck/rstcheck/pull/100)) +- Renamed CLI option `--report` to `--report-level` ([#100](https://github.com/rstcheck/rstcheck/pull/100)) +- Renamed CLI option `--ignore-language` to `--ignore-languages` ([#100](https://github.com/rstcheck/rstcheck/pull/100)) +- Drop CLI option `--ignore` as alias to `--ignore-languages` ([#100](https://github.com/rstcheck/rstcheck/pull/100)) +- Drop CLI option `--debug` ([#100](https://github.com/rstcheck/rstcheck/pull/100)) +- Drop CLI option `--version`; may be added back later ([#100](https://github.com/rstcheck/rstcheck/pull/100)) +- Don't support multi-line strings in INI files ([#100](https://github.com/rstcheck/rstcheck/pull/100)) +- Prohibit numbers as report level ([#100](https://github.com/rstcheck/rstcheck/pull/100)) +- Non-existing files are skipped; `rstcheck non-existing-file.rst` exits 0; may be changed later ([#100](https://github.com/rstcheck/rstcheck/pull/100)) +- Drop support for sphinx < 2.0 +- Drop default values for directives and roles for sphinx ([#65](https://github.com/rstcheck/rstcheck/issues/65)) +- CLI options now take precedence over config file options ([#96](https://github.com/rstcheck/rstcheck/issues/96)) + +### New features + +- Add section with `Known limitations / FAQ` to the README ([#97](https://github.com/rstcheck/rstcheck/issues/97)) +- Accumulate all errors in rst source instead of only one ([#83](https://github.com/rstcheck/rstcheck/issues/83)) +- Allow errors in code blocks to be ignored via ignore_messages ([#100](https://github.com/rstcheck/rstcheck/pull/100)) +- Add support for TOML config files ([#84](https://github.com/rstcheck/rstcheck/pull/84)) + +### Bugfixes + +- Fix inability to ignore `code`, `code-block` and `sourcecode` directives ([#79](https://github.com/rstcheck/rstcheck/issues/79)) +- Fix `code-block` options recognition ([#62](https://github.com/rstcheck/rstcheck/issues/62)) +- Fix Malformed tables because of substitutions ([#82](https://github.com/rstcheck/rstcheck/pull/82)) +- Fix: remove `include` directive from ignore list when sphinx is active ([#70](https://github.com/rstcheck/rstcheck/issues/70)) + +## [v5.0.0 (2022-04-17)](https://pypi.org/project/rstcheck/5.0.0/) + +[diff v4.1.0...v5.0.0](https://github.com/rstcheck/rstcheck/compare/v4.1.0...v5.0.0) + +- Add examples/ to sdist +- Add `Development` section to README and update `Testing` section +- Add `Mega-Linter` section to README +- Add `BREAKING CHANGES` sections to changelog + +### BREAKING CHANGES + +- Rewrite test.bash script in pytest test cases and run them on Linux in CI +- Rewrite old test suite in pytest and AAA style + +## [v4.1.0 (2022-04-16)](https://pypi.org/project/rstcheck/4.1.0/) + +[diff v4.0.0...v4.1.0](https://github.com/rstcheck/rstcheck/compare/v4.0.0...v4.1.0) + +- Fix shebangs and scripts to use `python3` instead of `python` ([#78](https://github.com/rstcheck/rstcheck/pull/78)) +- Improve the gcc checker functions by removing restrictions and + using environment variable flags ([#88](https://github.com/rstcheck/rstcheck/pull/88)) +- Fix pool size on windows by setting max to 61 ([#86](https://github.com/rstcheck/rstcheck/pull/86)) +- Update test.bash script and makefile with new file location + +## [v4.0.0 (2022-04-15)](https://pypi.org/project/rstcheck/4.0.0/) + +[diff v3.5.0...v4.0.0](https://github.com/rstcheck/rstcheck/compare/v3.5.0...v4.0.0) + +- Add inline type annotations +- Add `sphinx` as extra +- Update build process and set up `poetry` +- Add `pre-commit` and `tox` for automated testing, linting and formatting +- Move from travis to github actions +- Activate dependabot + +### BREAKING CHANGES + +- Drop support for python versions prior 3.7 + +## [v3.5.0 (2022-04-14)](https://pypi.org/project/rstcheck/3.5.0/) + +[diff v3.4.0...v3.5.0](https://github.com/rstcheck/rstcheck/compare/v3.4.0...v3.5.0) + +- Deprecate python versions prior 3.7 + +## [v3.4.0 (2022-04-12)](https://pypi.org/project/rstcheck/3.4.0/) + +[diff v3.3.1...v3.4.0](https://github.com/rstcheck/rstcheck/compare/v3.3.1...v3.4.0) + +- Add `--config` option to change the location of the config file. +- Add `pre-commit` hooks config. + +## [v3.3.1 (2018-11-09)](https://pypi.org/project/rstcheck/3.3.1/) + +[diff v3.3...v3.3.1](https://github.com/rstcheck/rstcheck/compare/v3.3...v3.3.1) + +- Make compatible with Sphinx >= 1.8. + +## [v3.3 (2018-03-17)](https://pypi.org/project/rstcheck/3.3/) + +[diff v3.2...v3.3](https://github.com/rstcheck/rstcheck/compare/v3.2...v3.3) + +- Parse more options from configuration file (thanks to Santos Gallegos). +- Allow ignoring specific (info/warning/error) messages via + `--ignore-messages` (thanks to Santos Gallegos). + +## [v3.2 (2018-02-17)](https://pypi.org/project/rstcheck/3.2/) + +[diff v3.1...v3.2](https://github.com/rstcheck/rstcheck/compare/v3.1...v3.2) + +- Check for invalid Markdown-style links (thanks to biscuitsnake). +- Allow configuration to be stored in `setup.cfg` (thanks to Maël Pedretti). +- Add `--recursive` option to recursively drill down directories to check for + all `*.rst` files. + +## [v3.1 (2017-03-08)](https://pypi.org/project/rstcheck/3.1/) + +[diff v3.0.1...v3.1](https://github.com/rstcheck/rstcheck/compare/v3.0.1...v3.1) + +- Add support for checking XML code blocks (thanks to Sameer Singh). + +## [v3.0.1 (2017-03-02)](https://pypi.org/project/rstcheck/3.0.1/) + +[diff v3.0...v3.0.1](https://github.com/rstcheck/rstcheck/compare/v3.0...v3.0.1) + +- Support UTF-8 byte order marks (BOM). Previously, `docutils` would + interpret the BOM as a visible character, which would lead to false positives + about underlines being too short. + +## [v3.0 (2016-12-19)](https://pypi.org/project/rstcheck/3.0/) + +[diff v2.2...v3.0](https://github.com/rstcheck/rstcheck/compare/v2.2...v3.0) + +- Optionally support Sphinx 1.5. Sphinx support will be enabled if Sphinx is + installed. + +## [v2.2 (2016-10-11)](https://pypi.org/project/rstcheck/2.2/) + +[diff v2.1...v2.2](https://github.com/rstcheck/rstcheck/compare/v2.1...v2.2) + +- Unknown + +## [v2.1 (2016-10-11)](https://pypi.org/project/rstcheck/2.1/) + +[diff v2.0...v2.1](https://github.com/rstcheck/rstcheck/compare/v2.0...v2.1) + +- Unknown + +## [v2.0 (2016-07-27)](https://pypi.org/project/rstcheck/2.0/) + +[diff v1.5.1...v2.0](https://github.com/rstcheck/rstcheck/compare/v1.5.1...v2.0) + +- Support loading settings from configuration files. + +## [v1.5.1 (2016-05-29)](https://pypi.org/project/rstcheck/1.5.1/) + +[diff v1.5...v1.5.1](https://github.com/rstcheck/rstcheck/compare/v1.5...v1.5.1) + +- Unknown + +## [v1.5 (2016-02-03)](https://pypi.org/project/rstcheck/1.5/) + +[diff v1.4.2...v1.5](https://github.com/rstcheck/rstcheck/compare/v1.4.2...v1.5) + +- Unknown + +## [v1.4.2 (2015-12-16)](https://pypi.org/project/rstcheck/1.4.2/) + +[diff v1.4.1...v1.4.2](https://github.com/rstcheck/rstcheck/compare/v1.4.1...v1.4.2) + +- Unknown + +## [v1.4.1 (2015-08-16)](https://pypi.org/project/rstcheck/1.4.1/) + +[diff v1.4...v1.4.1](https://github.com/rstcheck/rstcheck/compare/v1.4...v1.4.1) + +- Unknown + +## [v1.4 (2015-06-26)](https://pypi.org/project/rstcheck/1.4/) + +[diff v1.3.1...v1.4](https://github.com/rstcheck/rstcheck/compare/v1.3.1...v1.4) + +- Unknown + +## [v1.3.1 (2015-04-14)](https://pypi.org/project/rstcheck/1.3.1/) + +[diff v1.3...v1.3.1](https://github.com/rstcheck/rstcheck/compare/v1.3...v1.3.1) + +- Unknown + +## [v1.3 (2015-04-11)](https://pypi.org/project/rstcheck/1.3/) + +[diff v1.2.1...v1.3](https://github.com/rstcheck/rstcheck/compare/v1.2.1...v1.3) + +- Unknown + +## [v1.2.1 (2015-04-11)](https://pypi.org/project/rstcheck/1.2.1/) + +[diff v1.2...v1.2.1](https://github.com/rstcheck/rstcheck/compare/v1.2...v1.2.1) + +- Unknown + +## [v1.2 (2015-04-11)](https://pypi.org/project/rstcheck/1.2/) + +[diff v1.1.1...v1.2](https://github.com/rstcheck/rstcheck/compare/v1.1.1...v1.2) + +- Unknown + +## [v1.1.1 (2015-04-05)](https://pypi.org/project/rstcheck/1.1.1/) + +[diff v1.1...v1.1.1](https://github.com/rstcheck/rstcheck/compare/v1.1...v1.1.1) + +- Unknown + +## [v1.1 (2015-04-03)](https://pypi.org/project/rstcheck/1.1/) + +[diff v1.0...v1.1](https://github.com/rstcheck/rstcheck/compare/v1.0...v1.1) + +- Unknown + +## [v1.0 (2015-03-14)](https://pypi.org/project/rstcheck/1.0/) + +[diff v0.6...v1.0](https://github.com/rstcheck/rstcheck/compare/v0.6...v1.0) + +- Add Sphinx support. + +## [v0.6 (2014-09-25)](https://pypi.org/project/rstcheck/0.6/) + +[diff v0.5.1...v0.6](https://github.com/rstcheck/rstcheck/compare/v0.5.1...v0.6) + +- Unknown + +## [v0.5.1 (2014-08-23)](https://pypi.org/project/rstcheck/0.5.1/) + +[diff v0.5...v0.5.1](https://github.com/rstcheck/rstcheck/compare/v0.5...v0.5.1) + +- Unknown + +## [v0.5 (2014-06-01)](https://pypi.org/project/rstcheck/0.5/) + +[diff v0.4.1...v0.5](https://github.com/rstcheck/rstcheck/compare/v0.4.1...v0.5) + +- Unknown + +## [v0.4.1 (2014-05-31)](https://pypi.org/project/rstcheck/0.4.1/) + +[diff v0.4...v0.4.1](https://github.com/rstcheck/rstcheck/compare/v0.4...v0.4.1) + +- Unknown + +## [v0.4 (2014-05-24)](https://pypi.org/project/rstcheck/0.4/) + +[diff v0.3.6...v0.4](https://github.com/rstcheck/rstcheck/compare/v0.3.6...v0.4) + +- Unknown + +## [v0.3.6 (2014-04-12)](https://pypi.org/project/rstcheck/0.3.6/) + +[diff v0.3.5...v0.3.6](https://github.com/rstcheck/rstcheck/compare/v0.3.5...v0.3.6) + +- Unknown + +## [v0.3.5 (2014-01-25)](https://pypi.org/project/rstcheck/0.3.5/) + +[diff v0.3.4...v0.3.5](https://github.com/rstcheck/rstcheck/compare/v0.3.4...v0.3.5) + +- Unknown + +## [v0.3.4 (2013-12-29)](https://pypi.org/project/rstcheck/0.3.4/) + +[diff v0.3.3...v0.3.4](https://github.com/rstcheck/rstcheck/compare/v0.3.3...v0.3.4) + +- Unknown + +## [v0.3.3 (2013-12-28)](https://pypi.org/project/rstcheck/0.3.3/) + +[diff v0.3.2...v0.3.3](https://github.com/rstcheck/rstcheck/compare/v0.3.2...v0.3.3) + +- Unknown + +## [v0.3.2 (2013-12-27)](https://pypi.org/project/rstcheck/0.3.2/) + +[diff v0.3.1...v0.3.2](https://github.com/rstcheck/rstcheck/compare/v0.3.1...v0.3.2) + +- Unknown + +## v0.3.1 + +[diff v0.2...v0.3.1](https://github.com/rstcheck/rstcheck/compare/v0.2...v0.3.1) + +- Unknown + +## v0.2 + +[diff v0.1.1...v0.2](https://github.com/rstcheck/rstcheck/compare/v0.1.1...v0.2) + +- Unknown + +## v0.1.1 + +[diff v0.1...v0.1.1](https://github.com/rstcheck/rstcheck/compare/v0.1...v0.1.1) + +- Unknown + +## v0.1 (2013-12-02) + +[diff a146c93...v0.1](https://github.com/rstcheck/rstcheck/compare/a146c93...v0.1) + +- Initial version. diff --git a/LICENSE b/LICENSE index e08215d0..2fde5614 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (C) 2013-2017 Steven Myint +Copyright (C) 2013-2022 Steven Myint Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/MANIFEST.in b/MANIFEST.in index b654a409..a454a483 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,14 @@ +graft src +graft testing +graft tests +prune docs + +exclude * include AUTHORS.rst -include README.rst +include CHANGELOG.md include LICENSE +include MANIFEST.in +include pyproject.toml +include README.rst -exclude .travis.yml -exclude Makefile -include test.bash -include test_rstcheck.py -graft examples +global-exclude *~ *.py[cod] diff --git a/Makefile b/Makefile deleted file mode 100644 index 2e09e38f..00000000 --- a/Makefile +++ /dev/null @@ -1,31 +0,0 @@ -default: check test - - -# These checkers are more obscure. So consider them optional. Only run them if -# they are installed. -CHECK_MANIFEST := $(shell command -v check-manifest 2> /dev/null) -SCSPELL := $(shell command -v scspell 2> /dev/null) - - -check: - pycodestyle rstcheck.py setup.py - python setup.py --long-description | ./rstcheck.py - - ./rstcheck.py README.rst -ifdef CHECK_MANIFEST - $(CHECK_MANIFEST) -endif -ifdef SCSPELL - $(SCSPELL) rstcheck.py setup.py README.rst -endif - - -test: - ./test_rstcheck.py - time ./test.bash - - -readme: - @restview --long-description --strict - - -.PHONY: check test readme diff --git a/README.rst b/README.rst index a8c100f0..363fcda6 100644 --- a/README.rst +++ b/README.rst @@ -2,12 +2,27 @@ rstcheck ======== -.. image:: https://travis-ci.org/myint/rstcheck.svg?branch=master - :target: https://travis-ci.org/myint/rstcheck - :alt: Build status ++-------------------+---------------------------------------------------------------------------------------------+ +| **General** | |maintenance_y| |license| |semver| | +| +---------------------------------------------------------------------------------------------+ +| | |rtd| | ++-------------------+---------------------------------------------------------------------------------------------+ +| **CI** | |gha_tests| |gha_docu| |gha_qa| |pre_commit_ci| | ++-------------------+---------------------------------------------------------------------------------------------+ +| **PyPI** | |pypi_release| |pypi_py_versions| |pypi_implementations| | +| +---------------------------------------------------------------------------------------------+ +| | |pypi_format| |pypi_downloads| | ++-------------------+---------------------------------------------------------------------------------------------+ +| **Github** | |gh_tag| |gh_last_commit| | +| +---------------------------------------------------------------------------------------------+ +| | |gh_stars| |gh_forks| |gh_contributors| |gh_watchers| | ++-------------------+---------------------------------------------------------------------------------------------+ + Checks syntax of reStructuredText and code blocks nested within it. +See the full documentation at `read-the-docs`_ + .. contents:: @@ -15,10 +30,20 @@ Checks syntax of reStructuredText and code blocks nested within it. Installation ============ -From pip:: +From pip + +.. code:: shell $ pip install rstcheck +To use pyproject.toml for configuration:: + + $ pip install rstcheck[toml] + +To add sphinx support:: + + $ pip install rstcheck[sphinx] + Supported languages in code blocks ================================== @@ -36,6 +61,8 @@ Supported languages in code blocks Examples ======== +.. rstcheck: ignore-languages=cpp,python,rst + With bad Python syntax: .. code:: rst @@ -48,7 +75,7 @@ With bad Python syntax: print( -:: +.. code:: text $ rstcheck bad_python.rst bad_python.rst:7: (ERROR/3) (python) unexpected EOF while parsing @@ -68,7 +95,7 @@ With bad C++ syntax: return x; } -:: +.. code:: text $ rstcheck bad_cpp.rst bad_cpp.rst:9: (ERROR/3) (cpp) error: 'x' was not declared in this scope @@ -81,267 +108,103 @@ With bad syntax in the reStructuredText document itself: Test === -:: +.. code:: text $ rstcheck bad_rst.rst bad_rst.rst:1: (SEVERE/4) Title overline & underline mismatch. -Options -======= - -:: - - usage: rstcheck [-h] [--config CONFIG] [-r] [--report level] - [--ignore-language language] [--ignore-messages messages] - [--ignore-directives directives] - [--ignore-substitutions substitutions] [--ignore-roles roles] - [--debug] [--version] - files [files ...] - - Checks code blocks in reStructuredText. Sphinx is enabled. - - positional arguments: - files files to check - - optional arguments: - -h, --help show this help message and exit - --config CONFIG location of config file - -r, --recursive run recursively over directories - --report level report system messages at or higher than level; info, - warning, error, severe, none (default: info) - --ignore-language language, --ignore language - comma-separated list of languages to ignore - --ignore-messages messages - python regex that match the messages to ignore - --ignore-directives directives - comma-separated list of directives to ignore - --ignore-substitutions substitutions - comma-separated list of substitutions to ignore - --ignore-roles roles comma-separated list of roles to ignore - --debug show messages helpful for debugging - --version show program's version number and exit - - -Ignore specific languages -========================= - -You can ignore checking of nested code blocks by language. Either use the -command-line option ``--ignore`` or put a comment in the document: - -.. code-block:: rst - - .. rstcheck: ignore-language=cpp,python,rst - -Ignore specific errors -====================== - -Since docutils doesn't categorize their error messages beyond the high-level -categories of: info, warning, error, and severe; we need filter them out at a -textual level. This is done by passing a Python regex. As example you can pass -a regex like this to ignore several errors:: - - (Title underline too short.*|Duplicate implicit target.*') - -Configuration file -================== - -You can use the same arguments from the command line as options in the -local configuration file of the project (just replace ``-`` for ``_``). -``rstcheck`` looks for a file ``.rstcheck.cfg`` or ``setup.cfg`` in the -directory or ancestor directories of the file it is checking. - -For example, consider a project with the following directory structure:: - - foo - ├── docs - │ └── bar.rst - ├── index.rst - └── .rstcheck.cfg - -``.rstcheck.cfg`` contains: - -.. code-block:: cfg - - [rstcheck] - ignore_directives=one,two,three - ignore_roles=src,RFC - ignore_messages=(Document or section may not begin with a transition\.$) - report=warning - -``bar.rst`` contains: - -.. code-block:: rst - - Bar - === - - :src:`hello_world.py` - :RFC:`793` - - .. one:: - - Hello - -``rstcheck`` will make use of the ``.rstcheck.cfg``:: - - $ rstcheck foo/docs/bar.rst - - -For a Python project, you should put the configuration settings for -``rstcheck`` inside the general ``setup.cfg`` `distutils configuration file`_, -in the project root. - -You can override the location of the config file with the ``--config`` argument:: - - $ rstcheck --config $HOME/.rstcheck.ini foo/docs/bar.rst - -will use the file ``.rstcheck.ini`` in your home directory. If the argument to -``--config`` is a directory, ``rstcheck`` will search that directory and any -any of its ancestors for a file ``.rstcheck.cfg`` or ``setup.cfg``:: - - $ rstcheck --config foo /tmp/bar.rst - -would use the project configuration in ``./foo/.rstcheck.cfg`` to check the -unrelated file ``/tmp/bar.rst``. -Calling ``rstcheck`` with the ``--debug`` option will show the location of the -config file that is being used, if any. - -.. _distutils configuration file: https://docs.python.org/3/distutils/configfile.html - - -Sphinx -====== - -To enable Sphinx:: - - $ pip install sphinx - -The installed Sphinx version must be at least 1.5. - -To check that Sphinx support is enabled:: - - $ rstcheck -h | grep 'Sphinx is enabled' - - -Usage in Vim -============ - -Using with Syntastic_: ----------------------- - -.. code:: vim - - let g:syntastic_rst_checkers = ['rstcheck'] - -Using with ALE_: ----------------- - -Just install ``rstcheck`` and make sure is on your path. - -.. _Syntastic: https://github.com/scrooloose/syntastic -.. _ALE: https://github.com/w0rp/ale - - -Use as a module -=============== - -``rstcheck.check()`` yields a series of tuples. The first value of each tuple -is the line number (not the line index). The second value is the error message. - ->>> import rstcheck ->>> list(rstcheck.check('Example\n===')) -[(2, '(INFO/1) Possible title underline, too short for the title.')] - -Note that this does not load any configuration as that would mutate the -``docutils`` registries. - -Use as a pre-commit hook -======================== - -Add this to your ``.pre-commit-config.yaml`` - -.. code-block:: yaml - - - repo: https://github.com/myint/rstcheck - rev: '' # Use the sha / tag you want to point at - hooks: - - id: rstcheck +.. _read-the-docs: https://rstcheck.readthedocs.io -Testing -======= -To run all the tests, do:: +.. General - $ make test +.. |maintenance_n| image:: https://img.shields.io/badge/Maintenance%20Intended-✖-red.svg?style=flat-square + :target: http://unmaintained.tech/ + :alt: Maintenance - not intended -Unit tests are in ``test_rstcheck.py``. +.. |maintenance_y| image:: https://img.shields.io/badge/Maintenance%20Intended-✔-green.svg?style=flat-square + :target: http://unmaintained.tech/ + :alt: Maintenance - intended -System tests are composed of example good/bad input. The test inputs are -contained in the ``examples`` directory. For basic tests, adding a test should -just be a matter of adding files to ``examples/good`` or ``examples/bad``. +.. |license| image:: https://img.shields.io/github/license/rstcheck/rstcheck.svg?style=flat-square&label=License + :target: https://github.com/rstcheck/rstcheck/blob/main/LICENSE + :alt: License +.. |semver| image:: https://img.shields.io/badge/Semantic%20Versioning-2.0.0-brightgreen.svg?style=flat-square + :target: https://semver.org/ + :alt: Semantic Versioning - 2.0.0 -History -======= +.. |rtd| image:: https://img.shields.io/readthedocs/rstcheck/latest.svg?style=flat-square&logo=read-the-docs&logoColor=white&label=Read%20the%20Docs + :target: https://rstcheck.readthedocs.io/en/latest/ + :alt: Read the Docs - Build Status (latest) -(next version) --------------- -- Add ``--config`` option to change the location of the config file. +.. CI -3.3.1 (2018-10-09) ------------------- +.. |gha_tests| image:: https://img.shields.io/github/actions/workflow/status/rstcheck/rstcheck/test.yml?branch=main&style=flat-square&logo=github&label=Test%20code + :target: https://github.com/rstcheck/rstcheck/actions/workflows/test.yaml + :alt: Test status -- Make compatible with Sphinx >= 1.8. +.. |gha_docu| image:: https://img.shields.io/github/actions/workflow/status/rstcheck/rstcheck/documentation.yml?branch=main&style=flat-square&logo=github&label=Test%20documentation + :target: https://github.com/rstcheck/rstcheck/actions/workflows/documentation.yaml + :alt: Documentation status -3.3 (2018-03-17) ----------------- +.. |gha_qa| image:: https://img.shields.io/github/actions/workflow/status/rstcheck/rstcheck/qa.yml?branch=main&style=flat-square&logo=github&label=QA + :target: https://github.com/rstcheck/rstcheck/actions/workflows/qa.yaml + :alt: QA status -- Parse more options from configuration file (thanks to Santos Gallegos). -- Allow ignoring specific (info/warning/error) messages via - ``--ignore-messages`` (thanks to Santos Gallegos). +.. |pre_commit_ci| image:: https://results.pre-commit.ci/badge/github/rstcheck/rstcheck/main.svg + :target: https://results.pre-commit.ci/latest/github/rstcheck/rstcheck/main + :alt: pre-commit status -3.2 (2018-02-17) ----------------- -- Check for invalid Markdown-style links (thanks to biscuitsnake). -- Allow configuration to be stored in ``setup.cfg`` (thanks to Maël Pedretti). -- Add ``--recursive`` option to recursively drill down directories to check for - all ``*.rst`` files. +.. PyPI -3.1 (2017-03-08) ----------------- +.. |pypi_release| image:: https://img.shields.io/pypi/v/rstcheck.svg?style=flat-square&logo=pypi&logoColor=FBE072 + :target: https://pypi.org/project/rstcheck/ + :alt: PyPI - Package latest release -- Add support for checking XML code blocks (thanks to Sameer Singh). +.. |pypi_py_versions| image:: https://img.shields.io/pypi/pyversions/rstcheck.svg?style=flat-square&logo=python&logoColor=FBE072 + :target: https://pypi.org/project/rstcheck/ + :alt: PyPI - Supported Python Versions -3.0.1 (2017-03-01) ------------------- +.. |pypi_implementations| image:: https://img.shields.io/pypi/implementation/rstcheck.svg?style=flat-square&logo=python&logoColor=FBE072 + :target: https://pypi.org/project/rstcheck/ + :alt: PyPI - Supported Implementations -- Support UTF-8 byte order marks (BOM). Previously, ``docutils`` would - interpret the BOM as a visible character, which would lead to false positives - about underlines being too short. +.. |pypi_format| image:: https://img.shields.io/pypi/format/rstcheck.svg?style=flat-square&logo=pypi&logoColor=FBE072 + :target: https://pypi.org/project/rstcheck/ + :alt: PyPI - Format -3.0 (2016-12-19) ----------------- +.. |pypi_downloads| image:: https://img.shields.io/pypi/dm/rstcheck.svg?style=flat-square&logo=pypi&logoColor=FBE072 + :target: https://pypi.org/project/rstcheck/ + :alt: PyPI - Monthly downloads -- Optionally support Sphinx 1.5. Sphinx support will be enabled if Sphinx is - installed. -2.0 (2015-07-27) ----------------- -- Support loading settings from configuration files. +.. GitHub -1.0 (2015-03-14) ----------------- +.. |gh_tag| image:: https://img.shields.io/github/v/tag/rstcheck/rstcheck.svg?sort=semver&style=flat-square&logo=github + :target: https://github.com/rstcheck/rstcheck/tags + :alt: Github - Latest Release -- Add Sphinx support. +.. |gh_last_commit| image:: https://img.shields.io/github/last-commit/rstcheck/rstcheck.svg?style=flat-square&logo=github + :target: https://github.com/rstcheck/rstcheck/commits/main + :alt: GitHub - Last Commit -0.1 (2013-12-02) ----------------- +.. |gh_stars| image:: https://img.shields.io/github/stars/rstcheck/rstcheck.svg?style=flat-square&logo=github + :target: https://github.com/rstcheck/rstcheck/stargazers + :alt: Github - Stars -- Initial version. +.. |gh_forks| image:: https://img.shields.io/github/forks/rstcheck/rstcheck.svg?style=flat-square&logo=github + :target: https://github.com/rstcheck/rstcheck/network/members + :alt: Github - Forks +.. |gh_contributors| image:: https://img.shields.io/github/contributors/rstcheck/rstcheck.svg?style=flat-square&logo=github + :target: https://github.com/rstcheck/rstcheck/graphs/contributors + :alt: Github - Contributors -.. rstcheck: ignore-language=cpp,python,rst +.. |gh_watchers| image:: https://img.shields.io/github/watchers/rstcheck/rstcheck.svg?style=flat-square&logo=github + :target: https://github.com/rstcheck/rstcheck/watchers/ + :alt: Github - Watchers diff --git a/docs/source/_badges.rst b/docs/source/_badges.rst new file mode 100644 index 00000000..99b2daa7 --- /dev/null +++ b/docs/source/_badges.rst @@ -0,0 +1,109 @@ ++-------------------+---------------------------------------------------------------------------------------------+ +| **General** | |maintenance_y| |license| |semver| | +| +---------------------------------------------------------------------------------------------+ +| | |rtd| | ++-------------------+---------------------------------------------------------------------------------------------+ +| **CI** | |gha_tests| |gha_docu| |gha_qa| | ++-------------------+---------------------------------------------------------------------------------------------+ +| **PyPI** | |pypi_release| |pypi_py_versions| |pypi_implementations| | +| +---------------------------------------------------------------------------------------------+ +| | |pypi_format| |pypi_downloads| | ++-------------------+---------------------------------------------------------------------------------------------+ +| **Github** | |gh_tag| |gh_last_commit| | +| +---------------------------------------------------------------------------------------------+ +| | |gh_stars| |gh_forks| |gh_contributors| |gh_watchers| | ++-------------------+---------------------------------------------------------------------------------------------+ + + +.. Change badges in README also + +.. General + +.. Change maintenance status in README also + +.. |maintenance_n| image:: https://img.shields.io/badge/Maintenance%20Intended-✖-red.svg?style=flat-square + :target: http://unmaintained.tech/ + :alt: Maintenance - not intended + +.. |maintenance_y| image:: https://img.shields.io/badge/Maintenance%20Intended-✔-green.svg?style=flat-square + :target: http://unmaintained.tech/ + :alt: Maintenance - intended + +.. |license| image:: https://img.shields.io/github/license/rstcheck/rstcheck.svg?style=flat-square&label=License + :target: https://github.com/rstcheck/rstcheck/blob/main/LICENSE + :alt: License + +.. |semver| image:: https://img.shields.io/badge/Semantic%20Versioning-2.0.0-brightgreen.svg?style=flat-square + :target: https://semver.org/ + :alt: Semantic Versioning - 2.0.0 + +.. |rtd| image:: https://img.shields.io/readthedocs/rstcheck/latest.svg?style=flat-square&logo=read-the-docs&logoColor=white&label=Read%20the%20Docs + :target: https://rstcheck.readthedocs.io/en/latest/ + :alt: Read the Docs - Build Status (latest) + + +.. CI + + +.. |gha_tests| image:: https://img.shields.io/github/actions/workflow/status/rstcheck/rstcheck/test.yml?branch=main&style=flat-square&logo=github&label=Test%20code + :target: https://github.com/rstcheck/rstcheck/actions/workflows/test.yaml + :alt: Test status + +.. |gha_docu| image:: https://img.shields.io/github/actions/workflow/status/rstcheck/rstcheck/documentation.yml?branch=main&style=flat-square&logo=github&label=Test%20documentation + :target: https://github.com/rstcheck/rstcheck/actions/workflows/documentation.yaml + :alt: Documentation status + +.. |gha_qa| image:: https://img.shields.io/github/actions/workflow/status/rstcheck/rstcheck/qa.yml?branch=main&style=flat-square&logo=github&label=QA + :target: https://github.com/rstcheck/rstcheck/actions/workflows/qa.yaml + :alt: QA status + + +.. PyPI + +.. |pypi_release| image:: https://img.shields.io/pypi/v/rstcheck.svg?style=flat-square&logo=pypi&logoColor=FBE072 + :target: https://pypi.org/project/rstcheck/ + :alt: PyPI - Package latest release + +.. |pypi_py_versions| image:: https://img.shields.io/pypi/pyversions/rstcheck.svg?style=flat-square&logo=python&logoColor=FBE072 + :target: https://pypi.org/project/rstcheck/ + :alt: PyPI - Supported Python Versions + +.. |pypi_implementations| image:: https://img.shields.io/pypi/implementation/rstcheck.svg?style=flat-square&logo=python&logoColor=FBE072 + :target: https://pypi.org/project/rstcheck/ + :alt: PyPI - Supported Implementations + +.. |pypi_format| image:: https://img.shields.io/pypi/format/rstcheck.svg?style=flat-square&logo=pypi&logoColor=FBE072 + :target: https://pypi.org/project/rstcheck/ + :alt: PyPI - Format + +.. |pypi_downloads| image:: https://img.shields.io/pypi/dm/rstcheck.svg?style=flat-square&logo=pypi&logoColor=FBE072 + :target: https://pypi.org/project/rstcheck/ + :alt: PyPI - Monthly downloads + + + +.. GitHub + +.. |gh_tag| image:: https://img.shields.io/github/v/tag/rstcheck/rstcheck.svg?sort=semver&style=flat-square&logo=github + :target: https://github.com/rstcheck/rstcheck/tags + :alt: Github - Latest Release + +.. |gh_last_commit| image:: https://img.shields.io/github/last-commit/rstcheck/rstcheck.svg?style=flat-square&logo=github + :target: https://github.com/rstcheck/rstcheck/commits/main + :alt: GitHub - Last Commit + +.. |gh_stars| image:: https://img.shields.io/github/stars/rstcheck/rstcheck.svg?style=flat-square&logo=github + :target: https://github.com/rstcheck/rstcheck/stargazers + :alt: Github - Stars + +.. |gh_forks| image:: https://img.shields.io/github/forks/rstcheck/rstcheck.svg?style=flat-square&logo=github + :target: https://github.com/rstcheck/rstcheck/network/members + :alt: Github - Forks + +.. |gh_contributors| image:: https://img.shields.io/github/contributors/rstcheck/rstcheck.svg?style=flat-square&logo=github + :target: https://github.com/rstcheck/rstcheck/graphs/contributors + :alt: Github - Contributors + +.. |gh_watchers| image:: https://img.shields.io/github/watchers/rstcheck/rstcheck.svg?style=flat-square&logo=github + :target: https://github.com/rstcheck/rstcheck/watchers/ + :alt: Github - Watchers diff --git a/docs/source/authors.rst b/docs/source/authors.rst new file mode 100644 index 00000000..e9799a91 --- /dev/null +++ b/docs/source/authors.rst @@ -0,0 +1,4 @@ +Authors +======= + +.. include:: ../../AUTHORS.rst diff --git a/docs/source/changelog.md b/docs/source/changelog.md new file mode 100644 index 00000000..40c10426 --- /dev/null +++ b/docs/source/changelog.md @@ -0,0 +1,5 @@ +# Changelog File + +```{include} ../../CHANGELOG.md + +``` diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..21937963 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,155 @@ +"""Configuration file for the Sphinx documentation builder.""" + +from __future__ import annotations + +import datetime +import os +import re +import typing as t +from importlib.metadata import metadata +from importlib.util import find_spec +from pathlib import Path + +import sphinx_rtd_theme # type: ignore[import-untyped] + +if t.TYPE_CHECKING: + from sphinx.application import Sphinx + + +needs_sphinx = "3.1" #: Minimum Sphinx version to build the docs + + +#: -- GLOB VARS ------------------------------------------------------------------------ +NOT_LOADED_MSGS = [] + + +#: -- PROJECT INFORMATION -------------------------------------------------------------- +project = "rstcheck" +author = "Steven Myint " +GH_REPO_LINK = "https://github.com/rstcheck/rstcheck" +CREATION_YEAR = 2013 +CURRENT_YEAR = f"{datetime.datetime.now(tz=datetime.UTC).date().year}" +copyright = ( # noqa: A001 + f"{CREATION_YEAR}{('-' + CURRENT_YEAR) if CURRENT_YEAR != CREATION_YEAR else ''}, " + f"{author} and AUTHORS" +) +RSTCHECK_VERSION = metadata(project)["Version"] +release = RSTCHECK_VERSION #: The full version, including alpha/beta/rc tags +version_parts = re.search(r"^v?(?P\d+\.\d+)\.\d+[-.]?(?P[a-z]*)[\.]?\d*", release) +#: Major + Minor version like (X.Y) +version = None if not version_parts else version_parts.group("version") +#: only tags like alpha/beta/rc +RELEASE_LEVEL = None if not version_parts else version_parts.group("tag") + + +#: -- GENERAL CONFIG ------------------------------------------------------------------- +extensions: list[str] = [] +today_fmt = "%Y-%m-%d" +exclude_patterns: list[str] = [] #: Files to exclude for source of doc + +#: Added dirs for static and template files if they exist +html_static_path = ["_static"] if Path("_static").exists() else [] +templates_path = ["_templates"] if Path("_templates").exists() else [] + +rst_prolog = """ +.. ifconfig:: RELEASE_LEVEL in ('alpha', 'beta', 'rc') + + .. warning:: + The here documented release |release| is a pre-release. + Keep in mind that breaking changes can occur till the final release. + + You may want to use the latest stable release instead. +""" + +rst_epilog = """ +.. |br| raw:: html + +
+""" + +tls_cacerts = os.getenv("SSL_CERT_FILE") + + +#: -- MyST ----------------------------------------------------------------------------- +extensions.append("myst_parser") +source_suffix = { + ".rst": "restructuredtext", + ".txt": "markdown", + ".md": "markdown", +} + + +#: -- LINKCHECK CONFIG ----------------------------------------------------------------- +#: 1 Worker 5 Retries to fix 429 error +linkcheck_workers = 1 +linkcheck_retries = 5 +linkcheck_timeout = 30 + + +#: -- DEFAULT EXTENSIONS --------------------------------------------------------------- +#: Global +extensions.append("sphinx.ext.duration") +extensions.append("sphinx.ext.coverage") #: sphinx-build -b coverage ... +coverage_write_headline = False +coverage_show_missing_items = True +extensions.append("sphinx.ext.doctest") #: sphinx-build -b doctest ... + +#: ReStructuredText +extensions.append("sphinx.ext.autosectionlabel") +autosectionlabel_prefix_document = True +autosectionlabel_maxdepth = 2 +extensions.append("sphinx.ext.ifconfig") +extensions.append("sphinx.ext.viewcode") + +#: Links +extensions.append("sphinx.ext.intersphinx") +# NOTE: to inspect .inv files: https://github.com/bskinn/sphobjinv +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "pattern": ("https://docs.python.org/3/library/", "objects.pattern.inv"), +} + +extensions.append("sphinx.ext.extlinks") +extlinks = { + "repo": (f"{GH_REPO_LINK}/%s", "Repo's %s"), + "issue": (f"{GH_REPO_LINK}/issues/%s", "#%s"), + "pull": (f"{GH_REPO_LINK}/pull/%s", "PR#%s"), + "user": ("https://github.com/%s", "@%s"), +} + + +#: -- CLICK ---------------------------------------------------------------------------- +extensions.append("sphinx_click.ext") + + +#: -- SPELLING ------------------------------------------------------------------------- +spelling_word_list_filename = "../../spelling_dict.txt" +spelling_show_suggestions = True + +if find_spec("sphinxcontrib.spelling") is not None and os.environ.get("SPHINX_SPELLING") == "true": + extensions.append("sphinxcontrib.spelling") +else: + NOT_LOADED_MSGS.append("## 'sphinxcontrib-spelling' extension not loaded - not installed") + + +#: -- HTML THEME ----------------------------------------------------------------------- +#: needs install: "sphinx-rtd-theme" +extensions.append("sphinx_rtd_theme") +html_theme = "sphinx_rtd_theme" +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +html_theme_options = {"style_external_links": True, "navigation_depth": 5} + + +#: -- HTML OUTPUT ---------------------------------------------------------------------- +html_last_updated_fmt = today_fmt +html_show_sourcelink = True #: Add links to *.rst source files on HTML pages + + +#: -- FINAL SETUP ---------------------------------------------------------------------- +def setup(app: Sphinx) -> None: + """Connect custom func to sphinx events.""" + app.add_config_value("RELEASE_LEVEL", "", "env") + + +for msg in NOT_LOADED_MSGS: + print(msg) # noqa: T201 diff --git a/docs/source/faq.rst b/docs/source/faq.rst new file mode 100644 index 00000000..64ff2a84 --- /dev/null +++ b/docs/source/faq.rst @@ -0,0 +1,48 @@ +FAQ / Known limitations / Known issues +====================================== + +.. rstcheck: ignore-roles=issue + +You may also take a look at the `same section for rstcheck-core`_. + +Known limitations +----------------- + +There are inherent limitations to what ``rstcheck`` can and cannot do. The reason for this is that +``rstcheck`` itself relies on external tools for parsing and error reporting. +The rst source e.g. is given to ``docutils`` which then parses it and returns the errors. +Therefore rstcheck is more like an error accumulation tool. The same goes for the source +code in supported code blocks. + +.. note:: + + **Windows support is not stable!** + + Reason: Tests are failing with wrong positives and wrong negatives out of unknown reasons. + See issue :issue:`107`. + + +Known issues +------------ + +ImportError: cannot import name 'get_terminal_size' from 'click.termui' +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Affected rstcheck version(s): All 6.0 releases** + +This issue is caused by an incompatibility of the dependency ``typer`` in version ``0.4.0`` and its +subdependency ``click`` with version ``>=8.1.0``. +The issue was fixed in ``typer`` version ``0.4.1``. + +If you encounter this issue you can either: + +- update ``rstcheck`` to a non affected version. +- manually limit ``click``'s upper version-bound to ``<8.1`` if you need to rely on ``typer`` + ``<0.4.1``. +- manually limit ``typer``'s lower version-bound to ``>=0.4.1``. +- manually limit ``typer``'s upper version-bound to ``<0.4`` which results in ``typer`` version + ``0.3.2`` and ``click`` version ``<7.2``. + +See :issue:`138` for reference. + +.. _same section for rstcheck-core: https://rstcheck-core.readthedocs.io/en/latest/faq/ diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000..921e272b --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,28 @@ +Welcome to rstcheck's documentation! +==================================== + +.. include:: _badges.rst + + +This is the documentation of ``rstcheck``, a CLI application for checking +the syntax of reStructuredText and code blocks nested within it. + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + installation + usage/index + workflows/index + + +.. toctree:: + :maxdepth: 2 + :caption: Miscellaneous: + + faq + migrations + changelog + authors + license diff --git a/docs/source/installation.rst b/docs/source/installation.rst new file mode 100644 index 00000000..d82e31a5 --- /dev/null +++ b/docs/source/installation.rst @@ -0,0 +1,60 @@ +.. highlight:: console + +Installation +============ + +This part of the documentation covers how to install the package. +It is recommended to install the package in a virtual environment. + + +Create virtual environment +-------------------------- + +There are several packages/modules for creating python virtual environments. +Here is a manual_ by the PyPA. + + +Installation from PyPI +---------------------- + +You can simply install the package from PyPI:: + + $ pip install rstcheck + + +Extras +~~~~~~ + +``rstcheck`` has extras which can be installed to activate optional functionality: + +- ``sphinx`` - To activate support for rst documents using the sphinx builder. +- ``toml`` - To activate support for TOML files as configuration files. + +To install an extra simply add it in brackets like so:: + + $ pip install rstcheck[sphinx,toml] + + +Installation from source +------------------------ + +You can install ``rstcheck`` directly from a Git repository clone. +This can be done either by cloning the repository and installing from the local clone:: + + $ git clone https://github.com/rstcheck/rstcheck.git + $ cd rstcheck + $ pip install . + + +Or installing directly via git:: + + $ pip install git+https://github.com/rstcheck/rstcheck + + +You can also download the current version as `tar.gz` or `zip` file, extract it and +install it with pip like above. + +.. highlight:: default + + +.. _manual: https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/ diff --git a/docs/source/license.rst b/docs/source/license.rst new file mode 100644 index 00000000..42611002 --- /dev/null +++ b/docs/source/license.rst @@ -0,0 +1,4 @@ +License +======= + +.. literalinclude:: ../../LICENSE diff --git a/docs/source/migrations.rst b/docs/source/migrations.rst new file mode 100644 index 00000000..dd0c1e25 --- /dev/null +++ b/docs/source/migrations.rst @@ -0,0 +1,100 @@ +.. highlight:: console + +Migration-Guides +================ + +Only breaking changes are mentioned here. New features or fixes are only mentioned if they +somehow correspond to a breaking change. + + +.. contents:: + + +Version 5 to 6 +-------------- + +With version 6 the whole code base was restructured. The core library was moved into its own +repository at ``rstcheck/rstcheck-core``. + +``rstcheck`` moved from ``myint/rstcheck`` to ``rstcheck/rstcheck`` so you may want to update +links you have pointing to the old repository's location. + +The ``master`` branch was renamed to ``main``. If you use git dependencies you may need to update +your reference. + +The new logging system may help you find issues while migrating to v6. +You may then want to turn it on on the lowest level ``--log-level DEBUG``. + + +Configuration file +~~~~~~~~~~~~~~~~~~ + +- CLI options now overwrite settings in configuration files. Update your setup accordingly. + +- The following configuration keys changed: + + - ``report`` renamed to ``report_level`` + - ``ignore_language`` renamed to ``ignore_languages`` + +- Set the ``--warn-unknown-settings`` CLI flag for warnings on unknown settings in + configuration files. + +- Numbers are no longer supported for ``report_level`` (old: ``report``) + + +CLI +~~~ + +- CLI options now overwrite settings in configuration files. Update your setup accordingly. + +- Non existing files are ignored, but a warning is logged and the exit code is non-zero. + +-The following CLI options changed: + + - ``--report`` renamed to ``--report-level`` and no longer accepts integers + - ``--ignore-language`` renamed to ``--ignore-languages`` + - ``--ignore`` dropped -> use ``--ignore-languages`` + - ``--debug`` replaced with new ``--log-level`` -> use ``--log-level DEBUG`` for verbose output + +- Numbers are no longer supported for ``--report-level`` (old: ``--report``) + +- A non-existing path passed with ``--config`` results in a non-zero exit code. + + +Sphinx features +~~~~~~~~~~~~~~~ + +Support for sphinx prior version 2.0 was dropped. + +Hard-coded default values for roles and directives coming from ``sphinx`` were dropped. +If you encounter a lot of unknown roles and directives this may be the reason +(`Example issue`_). + +To fix this simply add sphinx to the environment from where you run ``rstcheck``:: + + $ pip install sphinx # directly + + $ pip install rstcheck[sphinx] # or via extra + +To check if sphinx support is activate run:: + + $ rstcheck --help | grep Sphinx + + +Version 4 to 5 +-------------- + +Nothing to do if you don't need the test suit of ``rstcheck``. + +Use ``tox`` to run test suite. + + +Version 3 to 4 +-------------- + +Use python 3.7 or newer to run ``rstcheck``. + +.. highlight:: default + + +.. _Example issue: https://github.com/rstcheck/rstcheck/issues/109 diff --git a/docs/source/objects.pattern.inv b/docs/source/objects.pattern.inv new file mode 100644 index 00000000..13ae09f7 Binary files /dev/null and b/docs/source/objects.pattern.inv differ diff --git a/docs/source/usage/cli.rst b/docs/source/usage/cli.rst new file mode 100644 index 00000000..c4738aaa --- /dev/null +++ b/docs/source/usage/cli.rst @@ -0,0 +1,33 @@ +CLI +=== + +This part of the documentation covers the CLI interface and +is auto-generated by sphinx-click extension. + +.. warning:: + + The CLI interface differs depending on the available opt-in extensions: + + - Sphinx + - Toml + + The documentation shown here uses all opt-in features. + +.. note:: + + More information about the single configuration options can be found in the + :ref:`usage/config:Configuration` section + +.. note:: + + If you get an exception and the pretty output makes the exception information unreadable try + to run rstcheck with the environment variable ``_TYPER_STANDARD_TRACEBACK`` set to ``1`` like + ``_TYPER_STANDARD_TRACEBACK=1 rstcheck somefile.rst```. + Read more at `typer's docs`_. + + +.. click:: rstcheck._cli:typer_click_object + :prog: rstcheck + :show-nested: + +.. _typer's docs: https://typer.tiangolo.com/tutorial/exceptions/#disable-pretty-exceptions diff --git a/docs/source/usage/config.rst b/docs/source/usage/config.rst new file mode 100644 index 00000000..baba4436 --- /dev/null +++ b/docs/source/usage/config.rst @@ -0,0 +1,377 @@ +Configuration +============= + +.. contents:: + +``rstcheck``'s config system knows three sources: + +- Inline comments (*for config and flow control instructions*) +- CLI options +- Config files (*INI and TOML*) + + +Order of application +-------------------- + +The config sources apply according to a set of rules: + +#. *Flow control instructions* from inline comments: + + - **always apply** regardless of other config. + - are **expressive** and say for themselves what they apply to. + +#. Config from inline comments: + + - **always apply** regardless of other config. + - **always apply for the whole file** regardless where they are placed. + - is **added** to the remaining config and does **not overwrite** it. + +#. CLI options **always overwrite** config coming from a file. +#. File config has the lowest priority. + + +Configuration sources +--------------------- + +Now let's take a deeper look at the different sources of config. + + +Inline comments +~~~~~~~~~~~~~~~ + +Inline comments are simply rst comments starting with ``rstcheck:``. +There are two types of inline comments: + +- Simple inline config e.g. ``ignore-languages=python`` which follows the syntax of ``key=value``. +- Flow control instructions e.g. ``ignore-next-code-block`` which follows the syntax of + ``words-divided-by-dashes``. + +Here is an example with both of them: + +.. code-block:: rst + + Example + ======= + + .. rstcheck: ignore-next-code-block + .. code-block:: python + + print("Hello World") + + .. rstcheck: ignore-languages=python + + +CLI options +~~~~~~~~~~~ + +For information on the CLI options please see the :ref:`usage/cli:CLI` section. + + +Configuration files +~~~~~~~~~~~~~~~~~~~ + +``rstcheck`` has an automatic config file detection mechanic. This mechanic includes +the following config files sorted by priority: + +#. .rstcheck.cfg +#. pyproject.toml +#. setup.cfg + +When a directory is searched for a config file, the first found config section is +taken and the search is not aborted on the first file found. +This means that if you for example have a pyproject.toml file without a matching config +section and an setup.cfg file with a matching section, the section from setup.cfg would be used. +pyproject.toml would be searched first, but nothing would be found and so the search would continue. + +For each rst source file that is linted, its parent directory is searched for a config file. +If the parent directory has no config file with a matching config section the parent's +parent directory is searched next. This continues up the directory tree until the root directory. +If no config file is found, the default values for each setting apply. + +This whole mechanic is deactivated, when a config file or directory is explicitly set. +See the `Configuration file`_ section for more information on setting a config file/directory. + +``rstcheck`` supports two types of config formats: **INI** and **TOML**. +They are written pretty similar, but have some differences. +The two following sections explain both formats. + +Files ending on ``.toml`` are parsed as TOML files. +Every other file is parsed as an INI file. + +If ``.rstcheck.cfg`` does not contain a valid section a warning is printed. + + +INI format +^^^^^^^^^^ + +In INI format all config related to ``rstcheck`` must go into a ``[rstcheck]`` section. + +The default INI format applies: ``key = value``. +Lists are comma-separated strings, which can be multiline. +Whitespace before and after a key or value is ignored. +Trailing commas are optional. + +Here is an example: + +.. code-block:: ini + + [rstcheck] + report_level=WARNING + ignore_directives = + one, + two, + three, + ignore_roles=src, RFC + ignore_substitutions= + image_link + ignore_languages= + python, + cpp + ignore_messages=(Document or section may not begin with a transition\.$) + + +TOML format +^^^^^^^^^^^ + +.. note:: + + TOML format is only supported when the python library ``tomli`` is importable. + See the :ref:`installation:Installation` section for more information. + +In TOML format all config related to ``rstcheck`` must go into the ``[tool.rstcheck]`` +dictionary. This is due to the python convention for the ``pyproject.toml`` file, which +``rstcheck`` uses for all TOML files. + +The official TOML syntax applies here, so strings are strings and lists are lists for example. + +Here is an example: + +.. code-block:: toml + + [tool.rstcheck] + report_level = "WARNING" + ignore_directives = [ + "one", + "two", + "three", + ] + ignore_roles = ["src", "RFC"] + ignore_substitutions = [ + "image_link" + ] + ignore_languages = [ + "python", + "cpp" + ] + ignore_messages = "(Document or section may not begin with a transition\.$)" + + +Configuration options +--------------------- + +Now it's time for all the available settings you can set. + + +Configuration file +~~~~~~~~~~~~~~~~~~ + +Supported sources: + +- CLI (``--config PATH`` ) + +With the ``--config`` CLI option you can set a config file or directory. +The path may be relative or absolute. + +If the passed path does not exist the runner exits with an error, which is logged. + +If the path is a literal ``NONE``, no file is loaded or directory searched, this includes +the automatic config file detection mechanic. + +When the path points to a file, this concrete file is read and searched for a matching +config section. +If no section is found a warning is logged and no file config is used. + +When the path is a directory, this directory is search for a config file, like described +in the earlier `Configuration files`_ section, except that only this directory is search and +not the directory tree. + + +Recursive resolution +~~~~~~~~~~~~~~~~~~~~ + +Supported sources: + +- CLI (``--recursive`` or ``-r``) + +By default only files passed to the CLI runner are checked and directories are ignored. +When this config is set, passed directories are searched recursively for rst source files. + + +Report level +~~~~~~~~~~~~ + +Supported sources: + +- CLI (``--report-level LEVEL``) +- Config-File (key: ``report_level``, value: LEVEL) + +The level at which linting issues should be printed. The following levels are supported: + +- INFO (default) +- WARNING +- ERROR +- SEVERE +- NONE + +This currently only applies to issues with rst source. +Issues in code blocks are on ERROR level and always printed, +even if the level is set to SEVERE or NONE. + +The level can be set case insensitive. + + +Logging level +~~~~~~~~~~~~~ + +Supported sources: + +- CLI (``--log-level LEVEL``) + +The level at which additional information besides linting issues should be printed. +The following levels are supported: + +- DEBUG +- INFO +- WARNING (default) +- ERROR +- CRITICAL + +The level can be set case insensitive. + + +Ignore directives +~~~~~~~~~~~~~~~~~ + +Supported sources: + +- Inline comments (key: ``ignore-directives``, value: list of directives) +- CLI (``--ignore-directives D1,D2,...``) +- Config-File (key: ``ignore_directives``, value: list of directives) + +A list of directives to ignore while checking rst source. + + +Ignore roles +~~~~~~~~~~~~ + +Supported sources: + +- Inline comments (key: ``ignore-roles``, value: list of roles) +- CLI (``--ignore-roles R1,R2,...``) +- Config-File (key: ``ignore_roles``, value: list of roles) + +A list of roles to ignore while checking rst source. + + +Ignore substitutions +~~~~~~~~~~~~~~~~~~~~ + +Supported sources: + +- Inline comments (key: ``ignore-substitutions``, value: list of substitutions) +- CLI (``--ignore-substitutions S1,S2,...``) +- Config-File (key: ``ignore_substitutions``, value: list of substitutions) + +A list of substitutions to ignore while checking rst source. + + +Ignore specific code-block languages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Supported sources: + +- Inline comments (key: ``ignore-languages``, value: list of languages) +- CLI (``--ignore-languages L1,L2,...``) +- Config-File (key: ``ignore_languages``, value: list of languages) + +A list of languages to ignore for code blocks in rst source. +Unsupported languages are ignored automatically. + +Supported languages are: + +- Bash +- Doctest +- C (C99) +- C++ (C++11) +- JSON +- XML +- Python +- reStructuredText + + +Ignore specific error messages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Supported sources: + +- CLI (``--ignore-messages REGEX_STRING``) +- Config-File (key: ``ignore_messages``, value: regular expression string) + +A list of linting issue messages to ignore while checking rst source and code blocks. + +.. note:: + + In TOML format a list of strings is also valid. The list's entries will be + concatenated with the OR operator "|" between each entry. + + +Control Flow instructions +------------------------- + +There are also control flow instructions which are only available as inline comments. +They change the flow of checking the rst source, hence the name. + + +Skipping code blocks +~~~~~~~~~~~~~~~~~~~~ + +With the ``ignore-next-code-block`` flow control instruction you can skip single code blocks. +This way you don't have to use the heavy tools like ignoring a whole language or directive. + +The instruction **must** be placed in the line directly above the code block directive like so: + + +.. code-block:: rst + + .. rstcheck: ignore-next-code-block + .. code-block:: python + + print("Hello world") + + +Examples with explanation +------------------------- + +These examples are cases to show concepts of configuration in ``rstcheck``. +They don't always follow best practices. + + +Only inline comments +~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: rst + + Example + ======= + + .. rstcheck: ignore-next-code-block + .. code-block:: python + + print("Here is an error." + + .. rstcheck: ignore-languages=python + +In this example the code-block would be ignored/skipped due to the flow control instruction. +But the code-block's language is python which is on the ignore list for languages, because of the +config at the bottom. This means if you remove the flow control instruction, the +code-block would still be skipped and the error inside would ignored. diff --git a/docs/source/usage/index.rst b/docs/source/usage/index.rst new file mode 100644 index 00000000..320a1b69 --- /dev/null +++ b/docs/source/usage/index.rst @@ -0,0 +1,12 @@ +Usage +===== + +``rstcheck`` is a CLI application which uses the ``rstcheck-core`` library. + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + cli + config + integration diff --git a/docs/source/usage/integration.rst b/docs/source/usage/integration.rst new file mode 100644 index 00000000..124d272f --- /dev/null +++ b/docs/source/usage/integration.rst @@ -0,0 +1,53 @@ +Integration +=========== + +``rstcheck`` can be integrated and used with other tools. + + +Usage in Vim +------------ + + +Using with Syntastic_: +~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: vim + + let g:syntastic_rst_checkers = ['rstcheck'] + + +Using with ALE_: +~~~~~~~~~~~~~~~~ + +Just install ``rstcheck`` and make sure is on your path. + + +Use as a pre-commit hook +------------------------ + +Add this to your ``.pre-commit-config.yaml``: + +.. code:: yaml + + - repo: https://github.com/rstcheck/rstcheck + rev: '' # Use the sha / tag you want to point at + hooks: + - id: rstcheck + additional_dependencies: [] # can be omitted if empty + +You may want to specify a ``rstcheck-core`` version or range, if you depend on a feature which was +added after the current minimal version of ``rstcheck-core``. +Simply add e.g. ``"rstcheck-core==v1.0.0"`` to the list for ``additional_dependencies``. + + +Use with Mega-Linter +-------------------- + +Just install Mega-Linter_ in your repository, rstcheck_ is part of +the 70 linters activated out of the box. + + +.. _Syntastic: https://github.com/vim-syntastic/syntastic +.. _ALE: https://github.com/dense-analysis/ale +.. _Mega-Linter: https://oxsecurity.github.io/megalinter/latest/ +.. _rstcheck: https://oxsecurity.github.io/megalinter/latest/descriptors/rst_rstcheck/ diff --git a/docs/source/workflows/development.rst b/docs/source/workflows/development.rst new file mode 100644 index 00000000..e56a8717 --- /dev/null +++ b/docs/source/workflows/development.rst @@ -0,0 +1,87 @@ +.. highlight:: console + +Development +=========== + +``rstcheck`` uses `Semantic Versioning`_. + +``rstcheck`` uses ``main`` as its single development branch. Therefore releases are +made from this branch. Only the current release is supported and bugfixes are released +with a patch release for the current minor release. + + +Tooling +------- + +For development the following tools are used: + +- ``setuptools`` for package metadata and building. +- ``twine`` for publishing. +- ``tox`` for automated and isolated testing. +- ``pre-commit`` for automated QA checks via different linters and formatters. + + +Set up Local Development Environment +------------------------------------ + +Simply create a virtualenv and run ``pip install -e .[dev]``. This will install +``rstcheck`` along its main and development dependencies. + + +Working with the Local Development Environment +---------------------------------------------- + +Dependency management +~~~~~~~~~~~~~~~~~~~~~ + +Dependencies are listed in ``pyproject.toml`` and are manually managed. + + +Testing with tox +~~~~~~~~~~~~~~~~ + +To run all available tests and check simply run:: + + $ tox + +This will run: + +- formatters and linters via ``pre-commit``. +- the full test suite with ``pytest``. +- a test coverage report. +- tests for the documentation. + +Different environment lists are available and can be selected with ``tox -m ``: + +- test: run full test suite with ``pytest`` and report coverage. +- py`X`.`Y` run full test suite with specific python version and report coverage. +- docs: run all documentation tests. + + +Linting and formatting pre-commit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +can be used directly from within the development environment or you can use +``tox`` to run it pre-configured. + +There are 3 available ``tox`` envs with all use the same virtualenv: + +- ``pre-commit``: + For running any ``pre-commit`` command like ``tox -e pre-commit -- autoupdate --freeze``. +- ``pre-commit-run``: + For running all hooks against the code base. + A single hook's id can be passed as argument to run this hook only like + ``tox -e pre-commit-run -- black``. + + +IDE integration +~~~~~~~~~~~~~~~ + +The development environment has ``flakeheaven`` (a ``flake8`` wrapper), ``pylint`` and ``mypy`` +installed to allow IDEs to use them for inline error messages. Their config is in +``pyproject.toml``. To run them actively use ``pre-commit`` and/or ``tox``. + +.. highlight:: default + + +.. _Semantic Versioning: https://semver.org/ diff --git a/docs/source/workflows/index.rst b/docs/source/workflows/index.rst new file mode 100644 index 00000000..e0d06bc1 --- /dev/null +++ b/docs/source/workflows/index.rst @@ -0,0 +1,11 @@ +Workflows +========= + +Here is the documentation about different workflows for ``rstcheck``. + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + development + releases diff --git a/docs/source/workflows/releases.rst b/docs/source/workflows/releases.rst new file mode 100644 index 00000000..ffabb8cb --- /dev/null +++ b/docs/source/workflows/releases.rst @@ -0,0 +1,47 @@ +.. highlight:: console + +Releases +======== + +Release workflow +---------------- + +When enough changes and additions or time important fixes have accumulated on the +``main`` branch its time for a new release. The exact time is subject to the +judgment of the maintainer(s). + + +.. note:: + + Before starting the process of creating a new release make sure that all CI pipelines + are green for the current commit. + +#. Check if the ``CHANGELOG.md`` is up-to-date and all changes are noted. + +#. Run ``prep_release.py`` script to bump version, finalize ``CHANGELOG.md``, + commit the changes and create a new git tag:: + + $ python3 prep_release.py + + For the increase type there are three options: + + - ``patch`` / ``bugfix``: + for changes that **do not** add new functionality and are backwards compatible + - ``minor`` / ``feature``: + for changes that **do** add new functionality and are backwards compatible + - ``major`` / ``breaking``: + for changes that are **not** backwards compatible + +#. Build the sdist and wheel:: + + $ python -m build + +#. Publish package:: + + $ twine upload dist/* + +#. Push the commit and tag to github:: + + $ git push --follow-tags + +.. highlight:: default diff --git a/examples/custom/good_with_custom.rst b/examples/custom/good_with_custom.rst deleted file mode 100644 index 770e93e8..00000000 --- a/examples/custom/good_with_custom.rst +++ /dev/null @@ -1,4 +0,0 @@ -.. my-directive:: - - -:some-custom-thing:`testing` diff --git a/examples/with_configuration/.rstcheck.cfg b/examples/with_configuration/.rstcheck.cfg deleted file mode 100644 index 57b59e02..00000000 --- a/examples/with_configuration/.rstcheck.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[rstcheck] -ignore_directives= - foobar, - my-directive, -ignore_roles=some-custom-thing -ignore_messages=(Document or section may not begin with a transition\.$) -ignore_language=cpp -report=warning diff --git a/examples/with_configuration/bad.rst b/examples/with_configuration/bad.rst deleted file mode 100644 index bc530601..00000000 --- a/examples/with_configuration/bad.rst +++ /dev/null @@ -1,11 +0,0 @@ -.. my-custom-directive:: - - -:some-custom-thing:`testing` - -.. code-block:: cpp - - int main() - { - return x; - } diff --git a/examples/with_configuration/dummydir/readme.txt b/examples/with_configuration/dummydir/readme.txt deleted file mode 100644 index 6161cc3e..00000000 --- a/examples/with_configuration/dummydir/readme.txt +++ /dev/null @@ -1,2 +0,0 @@ -This directory exists only for the test -./rstcheck.py --config examples/with_configuration/dummydir examples/examples/without_configuration/good.rst diff --git a/examples/with_configuration/good.rst b/examples/with_configuration/good.rst deleted file mode 100644 index 3e2056e1..00000000 --- a/examples/with_configuration/good.rst +++ /dev/null @@ -1,11 +0,0 @@ -.. my-directive:: - - -:some-custom-thing:`testing` - -.. code-block:: cpp - - int main() - { - return x; - } diff --git a/examples/with_configuration/rstcheck.ini b/examples/with_configuration/rstcheck.ini deleted file mode 100644 index 87920670..00000000 --- a/examples/with_configuration/rstcheck.ini +++ /dev/null @@ -1,9 +0,0 @@ -[rstcheck] -# this is a copy of .rstcheck.cfg, but used for the without_configuration example -ignore_directives= - foobar, - my-directive, -ignore_roles=some-custom-thing -ignore_messages=(Document or section may not begin with a transition\.$) -ignore_language=cpp -report=warning diff --git a/examples/without_configuration/good.rst b/examples/without_configuration/good.rst deleted file mode 100644 index 3e2056e1..00000000 --- a/examples/without_configuration/good.rst +++ /dev/null @@ -1,11 +0,0 @@ -.. my-directive:: - - -:some-custom-thing:`testing` - -.. code-block:: cpp - - int main() - { - return x; - } diff --git a/prep_release.py b/prep_release.py new file mode 100644 index 00000000..bd0cc9ba --- /dev/null +++ b/prep_release.py @@ -0,0 +1,173 @@ +"""Script for preparing the repo for a new release.""" + +from __future__ import annotations + +import argparse +import datetime +import re +import subprocess +import sys +from pathlib import Path + +PATCH = ("patch", "bugfix") +MINOR = ("minor", "feature") +MAJOR = ("major", "breaking") + +REPO_URL = "https://github.com/rstcheck/rstcheck" + + +#: -- MAIN ----------------------------------------------------------------------------- +def bump_version(current_version: str, release_type: str = "patch") -> str: + """Bump the current version for the next release. + + :param release_type: type of release; + allowed values are: patch | minor/feature | major/breaking; + defaults to "patch" + :raises ValueError: when an invalid release_type is given. + :raises ValueError: when the version string from pyproject.toml is not parsable. + :return: new version string + """ + if release_type not in PATCH + MINOR + MAJOR: + msg = f"Invalid version increase type: {release_type}" + raise ValueError(msg) + + version_parts = re.match(r"v?(?P\d+)\.(?P\d+)\.(?P\d+)", current_version) + if not version_parts: + msg = f"Unparsable version: {current_version}" + raise ValueError(msg) + + if release_type in MAJOR: + version = f"{int(version_parts.group('major')) + 1}.0.0" + elif release_type in MINOR: + version = f"{version_parts.group('major')}.{int(version_parts.group('minor')) + 1}.0" + elif release_type in PATCH: + version = ( + f"{version_parts.group('major')}" + f".{version_parts.group('minor')}" + f".{int(version_parts.group('patch')) + 1}" + ) + else: + print("Given `RELEASE TYPE` is invalid.") # noqa: T201 + sys.exit(1) + + return f"v{version}" + + +def update_changelog(new_version: str, last_version: str, *, first_release: bool) -> None: + """Update CHANGELOG.md to be release ready. + + :param new_version: new version string + :param last_version: current version string + :first_release: if this is the first release + """ + with Path("CHANGELOG.md").open(encoding="utf8") as changelog_file: + changelog_lines = changelog_file.read().split("\n") + + release_line = 0 + + for idx, line in enumerate(changelog_lines): + if line.startswith("## Unreleased"): + release_line = idx + + if release_line: + today = datetime.datetime.now(tz=datetime.UTC).date().isoformat() + compare = f"{'' if first_release else ''}{last_version}...{new_version}" + changelog_lines[release_line] = ( + "## Unreleased\n" + "\n" + f"[diff {new_version}...main]" + f"({REPO_URL}/compare/{new_version}...main)\n" + "\n" + f"## [{new_version} ({today})]({REPO_URL}/releases/{new_version})\n" + "\n" + f"[diff {compare}]({REPO_URL}/compare/{compare})" + ) + + if len(changelog_lines) - 1 >= release_line + 2: + changelog_lines.pop(release_line + 1) # Remove blank line + changelog_lines.pop(release_line + 1) # Remove [diff ...] link line + + with Path("CHANGELOG.md").open("w", encoding="utf8") as changelog_file: + changelog_file.write("\n".join(changelog_lines)) + + +def commit_and_tag(version: str) -> None: + """Git commit and tag the new release.""" + subprocess.run( # noqa: S603 + [ # noqa: S607 + "git", + "commit", + "--no-verify", + f'--message="release {version} [skip ci]"', + "--include", + "pyproject.toml", + "CHANGELOG.md", + ], + check=True, + ) + subprocess.run( # noqa: S603 + ["git", "tag", "-am", f"'{version}'", version], # noqa: S607 + check=True, + ) + + +def _parser() -> argparse.Namespace: + """Create parser and return parsed args.""" + parser = argparse.ArgumentParser() + parser.add_argument( + "increase_type", + metavar="RELEASE TYPE", + default="patch", + nargs="?", + help=( + "Release type: patch/bugfix | minor/feature | major/breaking; " + "gets ignored on `--first-release`; " + "defaults to patch" + ), + ) + parser.add_argument( + "--first-release", + action="store_true", + help="Flag for first release to prevent version bumping.", + ) + return parser.parse_args() + + +def _main() -> int: + """Prepare release main routine.""" + args = _parser() + + if args.first_release: + release_version = "v1.0.0" + #: Get first commit + current_version = subprocess.run( # noqa: S603 + ["git", "rev-list", "--max-parents=0", "HEAD"], # noqa: S607 + check=True, + capture_output=True, + ).stdout.decode()[0:7] + else: + git_tags = ( + subprocess.run( # noqa: S603 + ["git", "tag", "--list"], # noqa: S607 + check=True, + capture_output=True, + cwd=Path(__file__).parent, + ) + .stdout.decode() + .split("\n") + ) + git_tags = [t for t in git_tags if t.startswith("v")] + git_tags.sort() + current_version = git_tags[-1] + release_version = bump_version(current_version, args.increase_type) + update_changelog( + release_version, + current_version, + first_release=args.first_release, + ) + commit_and_tag(release_version) + return 0 + + +if __name__ == "__main__": + sys.exit(_main()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..070259f9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,349 @@ +[build-system] +requires = ["setuptools>=61", "wheel", "setuptools_scm[toml]>=6.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "rstcheck" +requires-python = ">=3.8" +authors = [ + { name = "Steven Myint", email = "git@stevenmyint.com" }, +] +maintainers = [ + { name = "Christian Riedel", email = "cielquan@protonmail.com" }, +] +description = "Checks syntax of reStructuredText and code blocks nested within it" +readme = "README.rst" +license = { text = "MIT" } +classifiers = [ + "Topic :: Software Development :: Quality Assurance", +] +keywords = ["restructuredtext", "rst", "linter", "static-analysis"] +dynamic = ["version"] + +dependencies = [ + "rstcheck-core >=1.1", + "typer >=0.12.0", +] + +[project.scripts] +rstcheck = "rstcheck._cli:main" + +[project.optional-dependencies] +sphinx = ["sphinx >=5.0"] +toml = ["tomli >=2.0; python_version<='3.10'"] +testing = [ + "pytest >=7.2", + "pytest-cov >=3.0", + "coverage[toml] >=6.0", + "coverage-conditional-plugin >=0.5", + "pytest-sugar >=0.9.5", + "pytest-randomly >=3.0", +] +docs = [ + "sphinx >=5.0", + "myst-parser >=3", + "sphinx-rtd-theme >=1.2", + "sphinxcontrib-spelling >=7.3", + "sphinx-click >=4.0.3", + "sphinx-autobuild >=2021.3.14", +] +type-check = [ + "mypy >=1.0", +] +dev = [ + "rstcheck[sphinx,toml,testing,docs,type-check]", + "tox >=3.15", +] + +[project.urls] +Documentation = "https://rstcheck.readthedocs.io/en/latest/" +Repository = "https://github.com/rstcheck/rstcheck" +Changelog = "https://github.com/rstcheck/rstcheck/blob/main/CHANGELOG.md" + +[tool.setuptools_scm] +write_to = "src/rstcheck/__version__.py" + + +# -- MYPY CONFIG ---------------------------------------------------------------------- +[tool.mypy] +python_version = "3.12" +follow_imports = "silent" +disallow_any_generics = true +disallow_subclassing_any = true +disallow_untyped_defs = true +check_untyped_defs = true +strict_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_return_any = true +warn_unreachable = true +implicit_reexport = false +show_error_context = true +show_column_numbers = true +plugins = ["pydantic.mypy"] +exclude = "\\.bak\\." + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = false +warn_required_dynamic_aliases = true + + +# -- RUFF CONFIG ---------------------------------------------------------------------- +[tool.ruff] +target-version = "py312" +line-length = 100 +output-format = "full" +show-fixes = true +extend-exclude = [ + "*venv*/", + "*.bak.*", +] +src = ["src", "tests"] + +[tool.ruff.lint] +# MISSING +# flake8-aaa https://github.com/astral-sh/ruff/issues/3462 +# flake8-broken-line https://github.com/astral-sh/ruff/issues/3465 +# flake8-cognitive-complexity https://github.com/astral-sh/ruff/issues/2418 # max_cognitive_complexity = 10 +# flake8-docstrings via pydocstyle? +# flake8-rst-docstrings = ">=0.2.5" # RST... +# flake8-sql https://github.com/sqlfluff/sqlfluff/ +# flake8-typing-imports https://github.com/astral-sh/ruff/issues/2302 +# flake8-use-fstring partial via UP031/UP032 https://github.com/astral-sh/ruff/issues/2097 +# flake8-variables-names https://github.com/astral-sh/ruff/issues/3463 +# flake8-walrus https://github.com/astral-sh/ruff/issues/3464 +######### +select = [ + "A", # flake8-builtins + "AIR", # Airflow + "ANN", # flake8-annotations + "ARG", # flake8-unused-arguments + "ASYNC", # flake8-async + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "C90", # mccabe + # "COM", # flake8-commas # black does that + # "CPY", # flake8-copyright # not needed + "D", # pydocstyle + "DJ", # flake8-django + "DTZ", # flake8-datetimez + "E", # pycodestyle + "EM", # flake8-errmsg + "ERA", # flake8-eradicate + "EXE", # flake8-executable + "F", # pyflakes + "FA", # flake8-future-annotations + "FBT", # flake8-boolean-trap + # "FIX", # flake8-fixme # custom pre-commit hook does with RegEx + "FLY", # flynt + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "INP", # flake8-no-pep420 + "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "N", # pep8-naming + "NPY", # NumPy-specific rules + "PD", # flake8-vet + "PERF", # Perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PLC", # Pylint - Convention + "PLE", # Pylint - Error + "PLR", # Pylint - Refactor + "PLW", # Pylint - Warning + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "PYI", # flake8-pyi + "Q", # flake8-quotes + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "S", # flake8-bandit + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + # "T10", # flake8-debugger # pre-commit hook does that + "T20", # flake8-print + "TCH", # flake8-type-checking + "TD", # flake8-todos + "TID", # flake8-tidy-imports + "TRY", # tryceratops + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 +] +ignore = [ + # deactivate because unwanted + "ANN101", # type self + "ANN102", # type cls + "E501", # line length + "G00", # logging uses format strings + "PT011", # Use match for specific exceptions in pytest.raises + # Deactivated for ruff-fmt + "W191", # tab-indentation + "E111", # indentation-with-invalid-multiple + "E114", # indentation-with-invalid-multiple-comment + "E117", # over-indented + "D206", # indent-with-spaces + "D300", # triple-single-quotes + "TD001", # invalid-todo-tag + "TD002", # missing-todo-author + "TD003", # missing-todo-link + "Q000", # bad-quotes-inline-string + "Q001", # bad-quotes-multiline-string + "Q002", # bad-quotes-docstring + "Q003", # avoidable-escaped-quote + "COM812", # missing-trailing-comma + "COM819", # prohibited-trailing-comma + "PT011", # Use match for specific exceptions in pytest.raises + "ISC001", # single-line-implicit-string-concatenation + "ISC002", # multi-line-implicit-string-concatenation +] +unfixable = ["ERA001"] + +task-tags = ["TODO", "FIXME", "XXX", "NOTE", "BUG", "HACK", "CHANGE ME"] +# typing-modules = [] + +[tool.ruff.lint.per-file-ignores] +"**/tests/**" = [ + "ARG", # unused arguments + "PLR0913", # Too many arguments to function call + "PLR2004", # Magic value comparison + "S101", # assert used + "SLF001", # Private member accessed +] +"**/tests/**/*_test.py" = [ + "FBT001", # Boolean positional arg in function definition +] +"__init__.py" = [ + "D104", # Missing docstring in public package + "PLC0414", # useless-import-alias +] +"**/testing/examples/**" = [ + "ERA001", # commented out code +] +"docs/source/conf.py" = [ + "INP001", # implicit namespace +] +"__version__.py" = ["ALL"] + +[tool.ruff.lint.flake8-annotations] +suppress-dummy-args = true + +[tool.ruff.lint.flake8-bugbear] +extend-immutable-calls = [ + "typer.Argument", + "typer.Option", +] + +[tool.ruff.lint.flake8-builtins] +builtins-ignorelist = [ + "id", +] + +[tool.ruff.lint.flake8-import-conventions.extend-aliases] +"typing" = "t" + +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false + +[tool.ruff.lint.flake8-type-checking] +runtime-evaluated-base-classes = [ + "pydantic.BaseModel", +] + +[tool.ruff.lint.isort] +combine-as-imports = true +known-first-party = [] +known-third-party = [] +required-imports = [ + "from __future__ import annotations", +] + +[tool.ruff.lint.mccabe] +max-complexity = 20 + +[tool.ruff.lint.pep8-naming] +classmethod-decorators = [ + "pydantic.field_validator", + "pydantic.model_validator", +] +staticmethod-decorators = [] + +[tool.ruff.lint.pycodestyle] +ignore-overlong-task-comments = true +max-doc-length = 100 + +[tool.ruff.lint.pydocstyle] +convention = "pep257" +ignore-decorators = [ + "typing.overload", +] +property-decorators = [] + +[tool.ruff.lint.pylint] +max-args = 6 + + +# -- PYTEST CONFIG -------------------------------------------------------------------- +[tool.pytest.ini_options] +addopts = "-ra --showlocals" +mock_use_standalone_module = true +junit_family = "xunit2" +asyncio_mode = "strict" + + +# -- COVERAGE CONFIG ------------------------------------------------------------------ +[tool.coverage] +[tool.coverage.run] +plugins = ["coverage_conditional_plugin"] +branch = true +parallel = true +context = "static-context" +omit = [ + "tests/*", +] + +[tool.coverage.paths] +tox_combine = [ + "src/rstcheck", + "*/.tox/*/lib/python*/site-packages/rstcheck", + "*/.tox/pypy*/site-packages/rstcheck", + "*/.tox\\*\\Lib\\site-packages\\rstcheck", +] +local_combine = [ + "src/rstcheck", + "*/.venv/lib/python*/site-packages/rstcheck", + "*/.venv\\*\\Lib\\site-packages\\rstcheck", + "*/src/rstcheck", + "*\\src\\rstcheck", +] + +[tool.coverage.report] +show_missing = true +exclude_lines = [ + "# pragma: no cover", + "if __name__ == ['\"]__main__['\"]:", + "def __str__", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "if 0:", + "if False:", + "if TYPE_CHECKING:", + "if typing.TYPE_CHECKING:", + "if MYPY:", +] + +[tool.coverage.html] +show_contexts = true + +[tool.coverage.coverage_conditional_plugin.rules] +# use with: # pragma: +py-gte-310 = "sys_version_info >= (3, 10)" diff --git a/rstcheck.py b/rstcheck.py deleted file mode 100755 index 0be1cbd1..00000000 --- a/rstcheck.py +++ /dev/null @@ -1,991 +0,0 @@ -#!/usr/bin/env python - -# Copyright (C) 2013-2017 Steven Myint -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -"""Checks code blocks in reStructuredText.""" - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import argparse -import copy -import contextlib -import doctest -import io -import json -import locale -import multiprocessing -import os -import re -import shutil -import subprocess -import sys -import tempfile -import xml.etree.ElementTree - -try: - import configparser -except ImportError: - import ConfigParser as configparser - -import docutils.core -import docutils.io -import docutils.nodes -import docutils.parsers.rst -import docutils.utils -import docutils.writers - -try: - import sphinx - SPHINX_INSTALLED = sphinx.version_info >= (1, 5) -except (AttributeError, ImportError): - SPHINX_INSTALLED = False - -if SPHINX_INSTALLED: - import sphinx.application - import sphinx.directives - import sphinx.domains.c - import sphinx.domains.cpp - import sphinx.domains.javascript - import sphinx.domains.python - import sphinx.domains.std - import sphinx.roles - - -__version__ = '3.3.1' - - -if SPHINX_INSTALLED: - SPHINX_CODE_BLOCK_DELTA = -1 - -RSTCHECK_COMMENT_RE = re.compile(r'\.\. rstcheck:') - - -# This is for the cases where code in a readme uses includes in that directory. -INCLUDE_FLAGS = ['-I.', '-I..'] -CONFIG_FILES = ['.rstcheck.cfg', - 'setup.cfg'] - - -class Error(Exception): - - """rstcheck exception.""" - - def __init__(self, message, line_number): - self.line_number = line_number - Exception.__init__(self, message) - - -class CodeBlockDirective(docutils.parsers.rst.Directive): - - """Code block directive.""" - - has_content = True - optional_arguments = 1 - - def run(self): - """Run directive.""" - try: - language = self.arguments[0] - except IndexError: - language = '' - code = '\n'.join(self.content) - literal = docutils.nodes.literal_block(code, code) - literal['classes'].append('code-block') - literal['language'] = language - return [literal] - - -def register_code_directive(): - """Register code directive.""" - if not SPHINX_INSTALLED: - docutils.parsers.rst.directives.register_directive('code', - CodeBlockDirective) - docutils.parsers.rst.directives.register_directive('code-block', - CodeBlockDirective) - docutils.parsers.rst.directives.register_directive('sourcecode', - CodeBlockDirective) - - -def strip_byte_order_mark(text): - """Return text with byte order mark (BOM) removed.""" - try: - return text.encode('utf-8').decode('utf-8-sig') - except UnicodeError: - return text - - -def check(source, - filename='', - report_level=docutils.utils.Reporter.INFO_LEVEL, - ignore=None, - debug=False): - """Yield errors. - - Use lower report_level for noisier error output. - - Each yielded error is a tuple of the form: - - (line_number, message) - - Line numbers are indexed at 1 and are with respect to the full RST file. - - Each code block is checked asynchronously in a subprocess. - - Note that this function mutates state by calling the ``docutils`` - ``register_*()`` functions. - - """ - # Do this at call time rather than import time to avoid unnecessarily - # mutating state. - register_code_directive() - ignore_sphinx() - - ignore = ignore or {} - - try: - ignore.setdefault('languages', []).extend( - find_ignored_languages(source) - ) - except Error as error: - yield (error.line_number, '{}'.format(error)) - - writer = CheckWriter(source, filename, ignore=ignore) - - string_io = io.StringIO() - - # This is a hack to avoid false positive from docutils (#23). docutils - # mistakes BOMs for actual visible letters. This results in the "underline - # too short" warning firing. - source = strip_byte_order_mark(source) - - try: - docutils.core.publish_string( - source, writer=writer, - source_path=filename, - settings_overrides={'halt_level': report_level, - 'report_level': report_level, - 'warning_stream': string_io}) - except docutils.utils.SystemMessage: - pass - except AttributeError: - # Sphinx will sometimes throw an exception trying to access - # "self.state.document.settings.env". Ignore this for now until we - # figure out a better approach. - if debug: - raise - - for checker in writer.checkers: - for error in checker(): - yield error - - rst_errors = string_io.getvalue().strip() - if rst_errors: - for message in rst_errors.splitlines(): - try: - ignore_regex = ignore.get('messages', '') - if ignore_regex and re.search(ignore_regex, message): - continue - yield parse_gcc_style_error_message(message, - filename=filename, - has_column=False) - except ValueError: - continue - - -def find_ignored_languages(source): - """Yield ignored languages. - - Languages are ignored via comment. - - For example, to ignore C++, JSON, and Python: - - >>> list(find_ignored_languages(''' - ... Example - ... ======= - ... - ... .. rstcheck: ignore-language=cpp,json - ... - ... .. rstcheck: ignore-language=python - ... ''')) - ['cpp', 'json', 'python'] - - """ - for (index, line) in enumerate(source.splitlines()): - match = RSTCHECK_COMMENT_RE.match(line) - if match: - key_and_value = line[match.end():].strip().split('=') - if len(key_and_value) != 2: - raise Error('Expected "key=value" syntax', - line_number=index + 1) - - if key_and_value[0] == 'ignore-language': - for language in key_and_value[1].split(','): - yield language.strip() - - -def _check_file(parameters): - """Return list of errors.""" - (filename, args) = parameters - - if filename == '-': - contents = sys.stdin.read() - else: - with contextlib.closing( - docutils.io.FileInput(source_path=filename) - ) as input_file: - contents = input_file.read() - - args = load_configuration_from_file( - os.path.dirname(os.path.realpath(filename)), args) - - ignore_directives_and_roles(args.ignore_directives, args.ignore_roles) - - for substitution in args.ignore_substitutions: - contents = contents.replace('|{}|'.format(substitution), 'None') - - ignore = { - 'languages': args.ignore_language, - 'messages': args.ignore_messages, - } - all_errors = [] - for error in check(contents, - filename=filename, - report_level=args.report, - ignore=ignore, - debug=args.debug): - all_errors.append(error) - return (filename, all_errors) - - -def check_python(code): - """Yield errors.""" - try: - compile(code, '', 'exec') - except SyntaxError as exception: - yield (int(exception.lineno), exception.msg) - - -def check_json(code): - """Yield errors.""" - try: - json.loads(code) - except ValueError as exception: - message = '{}'.format(exception) - line_number = 0 - - found = re.search(r': line\s+([0-9]+)[^:]*$', message) - if found: - line_number = int(found.group(1)) - - yield (int(line_number), message) - - -def check_xml(code): - """Yield errors.""" - try: - xml.etree.ElementTree.fromstring(code) - except xml.etree.ElementTree.ParseError as exception: - message = '{}'.format(exception) - line_number = 0 - - found = re.search(r': line\s+([0-9]+)[^:]*$', message) - if found: - line_number = int(found.group(1)) - - yield (int(line_number), message) - - -def check_rst(code, ignore): - """Yield errors in nested RST code.""" - filename = '' - - for result in check(code, - filename=filename, - ignore=ignore): - yield result - - -def check_doctest(code): - """Yield doctest syntax errors. - - This does not run the test as that would be unsafe. Nor does this - check the Python syntax in the doctest. That could be purposely - incorrect for testing purposes. - - """ - parser = doctest.DocTestParser() - try: - parser.parse(code) - except ValueError as exception: - message = '{}'.format(exception) - match = re.match('line ([0-9]+)', message) - if match: - yield (int(match.group(1)), message) - - -def get_and_split(options, key, default=''): - """Return list of split and stripped strings.""" - return split_comma_separated(options.get(key, default)) - - -def split_comma_separated(text): - """Return list of split and stripped strings.""" - return [t.strip() for t in text.split(',') if t.strip()] - - -def _get_directives_and_roles_from_sphinx(): - """Return a tuple of Sphinx directive and roles.""" - if SPHINX_INSTALLED: - sphinx_directives = list(sphinx.domains.std.StandardDomain.directives) - sphinx_roles = list(sphinx.domains.std.StandardDomain.roles) - - for domain in [sphinx.domains.c.CDomain, - sphinx.domains.cpp.CPPDomain, - sphinx.domains.javascript.JavaScriptDomain, - sphinx.domains.python.PythonDomain]: - - sphinx_directives += list(domain.directives) + [ - '{}:{}'.format(domain.name, item) - for item in list(domain.directives)] - - sphinx_roles += list(domain.roles) + [ - '{}:{}'.format(domain.name, item) - for item in list(domain.roles)] - else: - sphinx_roles = [ - 'abbr', - 'command', - 'dfn', - 'doc', - 'download', - 'envvar', - 'file', - 'guilabel', - 'kbd', - 'keyword', - 'mailheader', - 'makevar', - 'manpage', - 'menuselection', - 'mimetype', - 'newsgroup', - 'option', - 'program', - 'py:func', - 'ref', - 'regexp', - 'samp', - 'term', - 'token'] - - sphinx_directives = [ - 'autosummary', - 'currentmodule', - 'centered', - 'c:function', - 'c:type', - 'include', - 'deprecated', - 'envvar', - 'glossary', - 'index', - 'no-code-block', - 'literalinclude', - 'hlist', - 'option', - 'productionlist', - 'py:function', - 'seealso', - 'toctree', - 'todo', - 'versionadded', - 'versionchanged'] - - return (sphinx_directives, sphinx_roles) - - -class IgnoredDirective(docutils.parsers.rst.Directive): - - """Stub for unknown directives.""" - - has_content = True - - def run(self): - """Do nothing.""" - return [] - - -def _ignore_role(name, rawtext, text, lineno, inliner, - options=None, content=None): - """Stub for unknown roles.""" - # pylint: disable=unused-argument - return ([], []) - - -def ignore_sphinx(): - """Register Sphinx directives and roles to ignore.""" - (directives, roles) = _get_directives_and_roles_from_sphinx() - - directives += [ - 'centered', - 'include', - 'deprecated', - 'index', - 'no-code-block', - 'literalinclude', - 'hlist', - 'seealso', - 'toctree', - 'todo', - 'versionadded', - 'versionchanged'] - - ext_autosummary = [ - 'autosummary', - 'currentmodule', - ] - - ignore_directives_and_roles(directives + ext_autosummary, - roles + ['ctype']) - - -def find_config(directory_or_file, debug=False): - """Return configuration filename. - - If `directory_or_file` is a file, return the real-path of that file. If it - is a directory, find the configuration (any file name in CONFIG_FILES) in - that directory or its ancestors. - """ - directory_or_file = os.path.realpath(directory_or_file) - if os.path.isfile(directory_or_file): - if debug: - print('using config file {}'.format(directory_or_file), - file=sys.stderr) - return directory_or_file - directory = directory_or_file - - while directory: - for filename in CONFIG_FILES: - candidate = os.path.join(directory, filename) - if os.path.exists(candidate): - if debug: - print('using config file {}'.format(candidate), - file=sys.stderr) - return candidate - - parent_directory = os.path.dirname(directory) - if parent_directory == directory: - break - else: - directory = parent_directory - - -def load_configuration_from_file(directory, args): - """Return new ``args`` with configuration loaded from file.""" - args = copy.copy(args) - - directory_or_file = directory - if args.config is not None: - directory_or_file = args.config - - options = _get_options(directory_or_file, debug=args.debug) - - args.report = options.get('report', args.report) - threshold_dictionary = docutils.frontend.OptionParser.thresholds - args.report = int(threshold_dictionary.get(args.report, args.report)) - - args.ignore_language = get_and_split( - options, 'ignore_language', args.ignore_language) - - args.ignore_messages = options.get( - 'ignore_messages', args.ignore_messages) - - args.ignore_directives = get_and_split( - options, 'ignore_directives', args.ignore_directives) - - args.ignore_substitutions = get_and_split( - options, 'ignore_substitutions', args.ignore_substitutions) - - args.ignore_roles = get_and_split( - options, 'ignore_roles', args.ignore_roles) - - return args - - -def _get_options(directory_or_file, debug=False): - config_path = find_config(directory_or_file, debug=debug) - if not config_path: - return {} - - parser = configparser.ConfigParser() - parser.read(config_path) - try: - options = dict(parser.items('rstcheck')) - except configparser.NoSectionError: - return {} - else: - return options - - -def ignore_directives_and_roles(directives, roles): - """Ignore directives/roles in docutils.""" - for directive in directives: - docutils.parsers.rst.directives.register_directive(directive, - IgnoredDirective) - - for role in roles: - docutils.parsers.rst.roles.register_local_role(role, _ignore_role) - - -# The checker functions below return a checker. This is for purposes of -# asynchronous checking. As we visit each code block, a subprocess gets -# launched to run the checker. They all run in the background until we finish -# traversing the document. At that point, we accumulate the errors. - - -def bash_checker(code, working_directory): - """Return checker.""" - run = run_in_subprocess(code, '.bash', ['bash', '-n'], - working_directory=working_directory) - - def run_check(): - """Yield errors.""" - result = run() - if result: - (output, filename) = result - prefix = filename + ': line ' - for line in output.splitlines(): - if not line.startswith(prefix): - continue - message = line[len(prefix):] - split_message = message.split(':', 1) - yield (int(split_message[0]) - 1, - split_message[1].strip()) - return run_check - - -def c_checker(code, working_directory): - """Return checker.""" - return gcc_checker(code, '.c', - [os.getenv('CC', 'gcc'), '-std=c99'] + INCLUDE_FLAGS, - working_directory=working_directory) - - -def cpp_checker(code, working_directory): - """Return checker.""" - return gcc_checker(code, '.cpp', - [os.getenv('CXX', 'g++'), '-std=c++0x'] + INCLUDE_FLAGS, - working_directory=working_directory) - - -def gcc_checker(code, filename_suffix, arguments, working_directory): - """Return checker.""" - run = run_in_subprocess(code, - filename_suffix, - arguments + ['-pedantic', '-fsyntax-only'], - working_directory=working_directory) - - def run_check(): - """Yield errors.""" - result = run() - if result: - (output, filename) = result - for line in output.splitlines(): - try: - yield parse_gcc_style_error_message(line, - filename=filename) - except ValueError: - continue - - return run_check - - -def parse_gcc_style_error_message(message, filename, has_column=True): - """Parse GCC-style error message. - - Return (line_number, message). Raise ValueError if message cannot be - parsed. - - """ - colons = 2 if has_column else 1 - prefix = filename + ':' - if not message.startswith(prefix): - raise ValueError() - message = message[len(prefix):] - split_message = message.split(':', colons) - line_number = int(split_message[0]) - return (line_number, - split_message[colons].strip()) - - -def get_encoding(): - """Return preferred encoding.""" - return locale.getpreferredencoding() or sys.getdefaultencoding() - - -def run_in_subprocess(code, filename_suffix, arguments, working_directory): - """Return None on success.""" - temporary_file = tempfile.NamedTemporaryFile(mode='wb', - suffix=filename_suffix) - temporary_file.write(code.encode('utf-8')) - temporary_file.flush() - - process = subprocess.Popen(arguments + [temporary_file.name], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=working_directory) - - def run(): - """Yield errors.""" - raw_result = process.communicate() - if process.returncode != 0: - return (raw_result[1].decode(get_encoding()), - temporary_file.name) - - return run - - -class CheckTranslator(docutils.nodes.NodeVisitor): - - """Visits code blocks and checks for syntax errors in code.""" - - def __init__(self, document, contents, filename, ignore): - docutils.nodes.NodeVisitor.__init__(self, document) - self.checkers = [] - self.contents = contents - self.filename = filename - self.working_directory = os.path.dirname(os.path.realpath(filename)) - self.ignore = ignore or {} - self.ignore.setdefault('languages', []).append(None) - - def visit_doctest_block(self, node): - """Check syntax of doctest.""" - if 'doctest' in self.ignore['languages']: - return - - self._add_check(node=node, - run=lambda: check_doctest(node.rawsource), - language='doctest', - is_code_node=False) - - def visit_literal_block(self, node): - """Check syntax of code block.""" - # For "..code-block:: language" - language = node.get('language', None) - is_code_node = False - if not language: - # For "..code:: language" - is_code_node = True - classes = node.get('classes') - if 'code' in classes: - language = classes[-1] - else: - return - - if language in self.ignore['languages']: - return - - if language == 'doctest' or ( - language == 'python' and - node.rawsource.lstrip().startswith('>>> ')): - self.visit_doctest_block(node) - raise docutils.nodes.SkipNode - - checker = { - 'bash': bash_checker, - 'c': c_checker, - 'cpp': cpp_checker, - 'json': lambda source, _: lambda: check_json(source), - 'xml': lambda source, _: lambda: check_xml(source), - 'python': lambda source, _: lambda: check_python(source), - 'rst': lambda source, _: lambda: check_rst(source, - ignore=self.ignore) - }.get(language) - - if checker: - run = checker(node.rawsource, self.working_directory) - self._add_check(node=node, - run=run, - language=language, - is_code_node=is_code_node) - - raise docutils.nodes.SkipNode - - def visit_paragraph(self, node): - """Check syntax of reStructuredText.""" - find = re.search(r'\[[^\]]+\]\([^\)]+\)', node.rawsource) - if find is not None: - self.document.reporter.warning( - '(rst) Link is formatted in Markdown style.', base_node=node) - - def _add_check(self, node, run, language, is_code_node): - """Add checker that will be run.""" - def run_check(): - """Yield errors.""" - all_results = run() - if all_results is not None: - if all_results: - for result in all_results: - error_offset = result[0] - 1 - - line_number = getattr(node, 'line', None) - if line_number is not None: - yield ( - beginning_of_code_block( - node=node, - line_number=line_number, - full_contents=self.contents, - is_code_node=is_code_node) + - error_offset, - '({}) {}'.format(language, result[1])) - else: - yield (self.filename, 0, 'unknown error') - self.checkers.append(run_check) - - def unknown_visit(self, node): - """Ignore.""" - - def unknown_departure(self, node): - """Ignore.""" - - -def beginning_of_code_block(node, line_number, full_contents, is_code_node): - """Return line number of beginning of code block.""" - if SPHINX_INSTALLED and not is_code_node: - delta = len(node.non_default_attributes()) - current_line_contents = full_contents.splitlines()[line_number:] - blank_lines = next( - (i for (i, x) in enumerate(current_line_contents) if x), - 0) - return ( - line_number + - delta - 1 + - blank_lines - 1 + - SPHINX_CODE_BLOCK_DELTA) - else: - lines = full_contents.splitlines() - code_block_length = len(node.rawsource.splitlines()) - - try: - # Case where there are no extra spaces. - if lines[line_number - 1].strip(): - return line_number - code_block_length + 1 - except IndexError: - pass - - # The offsets are wrong if the RST text has multiple blank lines after - # the code block. This is a workaround. - for line_number in range(line_number, 1, -1): - if lines[line_number - 2].strip(): - break - - return line_number - code_block_length - - -class CheckWriter(docutils.writers.Writer): - - """Runs CheckTranslator on code blocks.""" - - def __init__(self, contents, filename, ignore): - docutils.writers.Writer.__init__(self) - self.checkers = [] - self.contents = contents - self.filename = filename - self.ignore = ignore - - def translate(self): - """Run CheckTranslator.""" - visitor = CheckTranslator(self.document, - contents=self.contents, - filename=self.filename, - ignore=self.ignore) - self.document.walkabout(visitor) - self.checkers += visitor.checkers - - -def decode_filename(filename): - """Return Unicode filename.""" - if hasattr(filename, 'decode'): - return filename.decode(sys.getfilesystemencoding()) - else: - return filename - - -def parse_args(): - """Return parsed command-line arguments.""" - threshold_choices = docutils.frontend.OptionParser.threshold_choices - - parser = argparse.ArgumentParser( - description=__doc__ + (' Sphinx is enabled.' - if SPHINX_INSTALLED else ''), - prog='rstcheck') - - parser.add_argument('files', nargs='+', type=decode_filename, - help='files to check') - parser.add_argument('--config', metavar='CONFIG', default=None, - help='location of config file') - parser.add_argument('-r', '--recursive', action='store_true', - help='run recursively over directories') - parser.add_argument('--report', metavar='level', - choices=threshold_choices, - default='info', - help='report system messages at or higher than ' - 'level; ' + - ', '.join(choice for choice in threshold_choices - if not choice.isdigit()) + - ' (default: %(default)s)') - parser.add_argument('--ignore-language', '--ignore', - metavar='language', default='', - help='comma-separated list of languages to ignore') - parser.add_argument('--ignore-messages', - metavar='messages', default='', - help='python regex that match the messages to ignore') - parser.add_argument('--ignore-directives', - metavar='directives', default='', - help='comma-separated list of directives to ignore') - parser.add_argument('--ignore-substitutions', - metavar='substitutions', default='', - help='comma-separated list of substitutions to ignore') - parser.add_argument('--ignore-roles', - metavar='roles', default='', - help='comma-separated list of roles to ignore') - parser.add_argument('--debug', action='store_true', - help='show messages helpful for debugging') - parser.add_argument('--version', action='version', - version='%(prog)s ' + __version__) - - args = parser.parse_args() - - if '-' in args.files: - if len(args.files) > 1: - parser.error("'-' for standard in can only be checked alone") - else: - args.files = list(find_files(filenames=args.files, - recursive=args.recursive)) - - return args - - -def output_message(text, file=sys.stderr): - """Output message to terminal.""" - if file.encoding is None: - # If the output file does not support Unicode, encode it to a byte - # string. On some machines, this occurs when Python is redirecting to - # file (or piping to something like Vim). - text = text.encode('utf-8') - - print(text, file=file) - - -@contextlib.contextmanager -def enable_sphinx_if_possible(): - """Register Sphinx directives and roles.""" - if SPHINX_INSTALLED: - srcdir = tempfile.mkdtemp() - outdir = os.path.join(srcdir, '_build') - try: - sphinx.application.Sphinx(srcdir=srcdir, - confdir=None, - outdir=outdir, - doctreedir=outdir, - buildername='dummy', - status=None) - yield - finally: - shutil.rmtree(srcdir) - else: - yield - - -def match_file(filename): - """Return True if file is okay for modifying/recursing.""" - base_name = os.path.basename(filename) - - if base_name.startswith('.'): - return False - - if not os.path.isdir(filename) and not filename.lower().endswith('.rst'): - return False - - return True - - -def find_files(filenames, recursive): - """Yield filenames.""" - while filenames: - name = filenames.pop(0) - if recursive and os.path.isdir(name): - for root, directories, children in os.walk(name): - filenames += [os.path.join(root, f) for f in children - if match_file(os.path.join(root, f))] - directories[:] = [d for d in directories - if match_file(os.path.join(root, d))] - else: - yield name - - -def main(): - """Return 0 on success.""" - args = parse_args() - - if not args.files: - return 0 - - with enable_sphinx_if_possible(): - status = 0 - pool = multiprocessing.Pool(multiprocessing.cpu_count()) - try: - if len(args.files) > 1: - results = pool.map( - _check_file, - [(name, args) for name in args.files]) - else: - # This is for the case where we read from standard in. - results = [_check_file((args.files[0], args))] - - for (filename, errors) in results: - for error in errors: - line_number = error[0] - message = error[1] - - if not re.match(r'\([A-Z]+/[0-9]+\)', message): - message = '(ERROR/3) ' + message - - output_message('{}:{}: {}'.format(filename, - line_number, - message)) - - status = 1 - except (IOError, UnicodeError) as exception: - output_message(exception) - status = 1 - finally: - pool.close() - - return status - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/setup.py b/setup.py deleted file mode 100755 index 43d9b2c1..00000000 --- a/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python - -"""Installer for rstcheck.""" - -import ast -import io - -import setuptools - - -def version(): - """Return version string.""" - with io.open('rstcheck.py', encoding='utf-8') as input_file: - for line in input_file: - if line.startswith('__version__'): - return ast.parse(line).body[0].value.s - - -with io.open('README.rst', encoding='utf-8') as readme: - setuptools.setup( - name='rstcheck', - version=version(), - url='https://github.com/myint/rstcheck', - description='Checks syntax of reStructuredText and code blocks nested ' - 'within it', - long_description=readme.read(), - classifiers=[ - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Topic :: Software Development :: Quality Assurance', - ], - keywords='restructuredtext,lint,check,pypi,readme,rst,analyze', - py_modules=['rstcheck'], - entry_points={'console_scripts': ['rstcheck = rstcheck:main']}, - install_requires=['docutils >= 0.7']) diff --git a/spelling_dict.txt b/spelling_dict.txt new file mode 100644 index 00000000..c7758b58 --- /dev/null +++ b/spelling_dict.txt @@ -0,0 +1,137 @@ +SkipNode +ValidationError +api +apidoc +arg +args +attr +autoapidoc +bool +boolean +bugfixes +cfg +changelog +ci +cielquan +cli +cmd +compat +conf +config +configs +contextmanager +cpp +csv +deleter +dependabot +dev +dict +dicts +dir +dirs +docstring +docstrings +doctest +docutils +dotenv +enum +enum's +env +envs +envvar +errorless +exe +executable +executables +expr +fmt +formatter +formatters +func +gcc +getter +github +html +importlib +ini +init +inv +json +kwarg +kwargs +linter +linters +lintersm +loglevel +macOS +makefile +monkeypatch +monkypatch +monkypatched +multiline +nd +noqa +nosec +nox +nox's +noxfile +num +numok +ok +opensource +os +parametrize +parsable +pragma +pre +prepended +py +pydantic +pyproject +pytest +pyyaml +rc +rd +reStructuredText +redef +repo +riedel +rst +rstcheck +rtd +sdist +setter +setuptools +sourcecode +sphinxcontrib +st +stderr +stdin +stdout +subdependency +submodule +submodules +subprocess +sys +temp +tempfile +testcleanup +testsetup +tmp +toctree +tokenize +toml +tomli +tomllib +tox +travis +txt +typer +unparsable +validator +venv +virtualenv +whitespace +xdist +xml +yaml diff --git a/src/rstcheck/__init__.py b/src/rstcheck/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/rstcheck/__main__.py b/src/rstcheck/__main__.py new file mode 100644 index 00000000..7592d338 --- /dev/null +++ b/src/rstcheck/__main__.py @@ -0,0 +1,13 @@ +"""Executes CLI application's `main` function. + +This module acts as the entry point for the application. When executed as the +main script, it calls the `main` function from the ._cli submodule to start the +application. +""" + +from __future__ import annotations + +from ._cli import main + +if __name__ == "__main__": + main() diff --git a/src/rstcheck/_cli.py b/src/rstcheck/_cli.py new file mode 100644 index 00000000..6fd020e5 --- /dev/null +++ b/src/rstcheck/_cli.py @@ -0,0 +1,177 @@ +"""CLI for rstcheck.""" + +from __future__ import annotations + +import logging +import pathlib +import typing as t +from importlib.metadata import version + +import typer +from rstcheck_core import _extras, config as config_mod, runner + +HELP_CONFIG = """Config file to load. Can be a INI file or directory. +If a directory is passed it will be searched for .rstcheck.cfg | setup.cfg. +If 'NONE' is passed no config file is loaded at all. +""" +if _extras.TOMLI_INSTALLED: # pragma: no cover + HELP_CONFIG = """Config file to load. Can be a INI or TOML file or directory. +If a directory is passed it will be searched for .rstcheck.cfg | pyproject.toml | setup.cfg. +If 'NONE' is passed no config file is loaded at all. +""" +HELP_WARN_UNKNOWN_SETTINGS = """Log a WARNING for unknown settings in config files. +Can be hidden via --log-level.""" +HELP_RECURSIVE = "Recursively search passed directories for RST files to check." +HELP_REPORT_LEVEL = f"""The report level of the linting issues found. +Valid levels are: INFO | WARNING | ERROR | SEVERE | NONE. +Defaults to {config_mod.DEFAULT_REPORT_LEVEL.name}. +Can be set in config file. +""" +HELP_LOG_LEVEL = """The log level of the application for information that is not a linting issue. +Valid levels are: DEBUG | INFO | WARNING | ERROR | CRITICAL. +Defaults to WARNING. +""" +HELP_IGNORE_DIRECTIVES = """Comma-separated-list of directives to add to the ignore list. +Can be set in config file. +""" +HELP_IGNORE_ROLES = """Comma-separated-list of roles to add to the ignore list. +Can be set in config file. +""" +HELP_IGNORE_SUBSTITUTIONS = """Comma-separated-list of substitutions to add to the ignore list. +Can be set in config file. +""" +HELP_IGNORE_LANGUAGES = """Comma-separated-list of languages for code-blocks to add to the ignore +list. The code in ignored code-blocks will not be checked for errors. +Can be set in config file. +""" +HELP_IGNORE_MESSAGES = """A regular expression to match linting issue messages against to ignore. +Can be set in config file. +""" +HELP_VERSION = "Print versions and exit." + + +def setup_logger(loglevel: str) -> None: + """Set up logging. + + :param loglevel: Level to log at. + :raises TypeError: On invalid logging levels. + """ + numeric_level = getattr(logging, loglevel.upper(), None) + if not isinstance(numeric_level, int): + msg = f"Invalid log level: {loglevel}" + raise TypeError(msg) + + logging.basicConfig(level=numeric_level) + + +def version_callback(value: bool) -> None: # noqa: FBT001 + """Print the version and exit.""" + if value: + typer.echo(f"rstcheck CLI Version: {version('rstcheck')}") + typer.echo(f"rstcheck-core Version: {version('rstcheck-core')}") + raise typer.Exit + + +def cli( # noqa: PLR0913 + files: t.List[pathlib.Path] = typer.Argument(..., allow_dash=True, hidden=True), # noqa: UP006 + config: t.Optional[pathlib.Path] = typer.Option( # noqa: UP007 + None, "--config", help=HELP_CONFIG + ), + warn_unknown_settings: t.Optional[bool] = typer.Option( # noqa: UP007 + None, "--warn-unknown-settings", help=HELP_WARN_UNKNOWN_SETTINGS + ), + recursive: t.Optional[bool] = typer.Option( # noqa: UP007 + None, "--recursive", "-r", help=HELP_RECURSIVE + ), + report_level: t.Optional[str] = typer.Option( # noqa: UP007 + None, metavar="LEVEL", help=HELP_REPORT_LEVEL + ), + # TODO: #i# use `t.Literal["INFO", "WARNING", "ERROR", "SEVERE", "NONE"]` when supported + log_level: str = typer.Option("WARNING", metavar="LEVEL", help=HELP_LOG_LEVEL), + ignore_directives: t.Optional[str] = typer.Option( # noqa: UP007 + None, help=HELP_IGNORE_DIRECTIVES + ), + ignore_roles: t.Optional[str] = typer.Option(None, help=HELP_IGNORE_ROLES), # noqa: UP007 + ignore_substitutions: t.Optional[str] = typer.Option( # noqa: UP007 + None, help=HELP_IGNORE_SUBSTITUTIONS + ), + ignore_languages: t.Optional[str] = typer.Option( # noqa: UP007 + None, help=HELP_IGNORE_LANGUAGES + ), + ignore_messages: t.Optional[str] = typer.Option( # noqa: UP007 + None, metavar="REGEX", help=HELP_IGNORE_MESSAGES + ), + version: t.Optional[bool] = typer.Option( # noqa: ARG001, UP007 + None, "--version", callback=version_callback, is_eager=True, help=HELP_VERSION + ), +) -> None: + """CLI of rstcheck.""" + setup_logger(log_level) + logger = logging.getLogger(__name__) + + if pathlib.Path("-") in files and len(files) > 1: + typer.echo("'-' is only allowed without additional files.", err=True) + raise typer.Abort + + logger.info("Create main configuration from CLI options.") + rstcheck_config = config_mod.RstcheckConfig( + config_path=config, + warn_unknown_settings=warn_unknown_settings, + recursive=recursive, + report_level=report_level, + ignore_directives=ignore_directives, + ignore_roles=ignore_roles, + ignore_substitutions=ignore_substitutions, + ignore_languages=ignore_languages, + ignore_messages=ignore_messages, + ) + + exit_code = 1 + + try: + logger.debug("Create main runner instance.") + _runner = runner.RstcheckMainRunner( + check_paths=files, rstcheck_config=rstcheck_config, overwrite_config=False + ) + logger.info("Run main runner instance.") + _runner.check() + exit_code = _runner.print_result() + + except FileNotFoundError as exc: + if exc.strerror == "Passed config path not found.": # pragma: no cover + logger.critical("Passed config path was not found: '%(path)s'", {"path": exc.filename}) + raise typer.Exit(code=1) from None + + raise + + raise typer.Exit(code=exit_code) + + +enabled_features = [] +if _extras.SPHINX_INSTALLED: + enabled_features.append("Sphinx") +if _extras.TOMLI_INSTALLED: # pragma: no cover + enabled_features.append("Toml") + +cli.__doc__ = f"""CLI of rstcheck. + +Enabled features: {enabled_features} + +Pass one or more RST FILES to check. +Can be files or directories if --recursive is passed too. +Pass "-" if you want to read from stdin. +""" + + +app = typer.Typer() +app.command()(cli) +typer_click_object = typer.main.get_command(app) + + +def main() -> None: # pragma: no cover + """Run CLI.""" + typer.run(cli) + + +if __name__ == "__main__": + main() diff --git a/src/rstcheck/py.typed b/src/rstcheck/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/test.bash b/test.bash deleted file mode 100755 index ec4c5388..00000000 --- a/test.bash +++ /dev/null @@ -1,103 +0,0 @@ -#!/bin/bash - -# System tests. - -set -eux - -trap "echo -e '\x1b[01;31mFailed\x1b[0m'" ERR - -for name in examples/good/*.rst -do - ./rstcheck.py "$name" -done - -for name in examples/bad/*.rst -do - if ./rstcheck.py "$name" - then - exit 1 - fi -done - -# Test multiple files. -./rstcheck.py examples/good/*.rst - -./rstcheck.py \ - --ignore-directives=my-directive \ - --ignore-role=some-custom-thing \ - examples/custom/good_with_custom.rst - -./rstcheck.py --ignore-language=cpp examples/bad/bad_cpp.rst - -./rstcheck.py - < examples/good/good.rst - -# Test multiple mix of good/bad files. -if ./rstcheck.py examples/bad/bad_cpp.rst examples/good/good.rst -then - exit 1 -fi - -# "-" should only be allowed to be checked alone. -if ./rstcheck.py - examples/good/good.rst -then - exit 1 -fi - -./rstcheck.py --report=none examples/bad/bad_rst.rst - -if ./rstcheck.py missing_file.rst -then - exit 1 -fi - -./rstcheck.py --recursive examples/good -if ./rstcheck.py --recursive examples/bad -then - exit 1 -fi - -# Test ignore messages -./rstcheck.py examples/bad/bad_rst.rst --ignore-messages '(Title .verline & underline mismatch\.$)' -if ./rstcheck.py examples/bad/bad_rst.rst --ignore-messages '(No match\.$)' -then - exit 1 -fi - -# Test configuration file -./rstcheck.py examples/with_configuration/good.rst -if ./rstcheck.py examples/with_configuration/bad.rst -then - exit 1 -fi - -# Ignore message on configuration file -./rstcheck.py examples/with_configuration/bad-2.rst - -# Ignore existing configuration file -# It's doesn't matter if /dev/null is empty or doesn't exist -if ./rstcheck.py --config=/dev/null examples/with_configuration/good.rst -then - exit 1 -fi - -# Check that we can use an unrelated configuration file -if ./rstcheck.py examples/without_configuration/good.rst -then - # the check should fail if we don't have a configuration - exit 1 -fi -# specify the folder of the config file -./rstcheck.py --debug --config examples/with_configuration examples/without_configuration/good.rst -# specify an explicit config file -./rstcheck.py --debug --config examples/with_configuration/rstcheck.ini examples/without_configuration/good.rst -# specify a folder from which we have to walk up -./rstcheck.py --debug --config examples/with_configuration/dummydir examples/without_configuration/good.rst - - -if python -c 'import sys; sys.exit(0 if sys.version_info >= (3,) else 1)' -then - python -m doctest -v README.rst rstcheck.py - ./rstcheck.py README.rst -fi - -echo -e '\x1b[01;32mOkay\x1b[0m' diff --git a/test_rstcheck.py b/test_rstcheck.py deleted file mode 100755 index 9d2e9cdd..00000000 --- a/test_rstcheck.py +++ /dev/null @@ -1,456 +0,0 @@ -#!/usr/bin/env python - -"""Test suite for rstcheck.""" - -from __future__ import unicode_literals - -import unittest - -import rstcheck - - -# We don't do this in the module itself to avoid mutation. -rstcheck.ignore_sphinx() - - -class Tests(unittest.TestCase): - - def assert_lines_equal(self, line_numbers, results): - self.assertEqual(set(line_numbers), set(dict(results))) - - def test_parse_gcc_style_error_message(self): - self.assertEqual( - (32, 'error message'), - rstcheck.parse_gcc_style_error_message( - 'filename:32:7: error message', - filename='filename')) - - def test_parse_gcc_style_error_message_with_no_column(self): - self.assertEqual( - (32, 'error message'), - rstcheck.parse_gcc_style_error_message( - 'filename:32: error message', - filename='filename', - has_column=False)) - - def test_parse_gcc_style_error_message_with_parsing_error(self): - with self.assertRaises(ValueError): - rstcheck.parse_gcc_style_error_message( - ':32:3 error message', - filename='filename') - - with self.assertRaises(IndexError): - rstcheck.parse_gcc_style_error_message( - 'filename:32: error message', - filename='filename', - has_column=True) - - def test_check(self): - self.assert_lines_equal( - [6], - rstcheck.check( - """\ -Test -==== - -.. code:: python - - print( -""")) - - def test_check_code_block(self): - self.assert_lines_equal( - [6], - rstcheck.check( - """\ -Test -==== - -.. code-block:: python - - print( -""")) - - def test_check_json(self): - self.assert_lines_equal( - [7], - rstcheck.check( - """\ -Test -==== - -.. code-block:: json - - { - 'abc': 123 - } -""")) - - def test_check_json_with_ignore(self): - self.assert_lines_equal( - [], - rstcheck.check( - """\ -Test -==== - -.. code-block:: json - - { - 'abc': 123 - } - -.. rstcheck: ignore-language=json,python,rst -""")) - - def test_check_json_with_unmatched_ignores_only(self): - self.assert_lines_equal( - [7], - rstcheck.check( - """\ -Test -==== - -.. code-block:: json - - { - 'abc': 123 - } - -.. rstcheck: ignore-language=cpp,python,rst -""")) - - def test_check_json_with_bad_ignore(self): - self.assert_lines_equal( - [7, 10], - rstcheck.check( - """\ -Test -==== - -.. code-block:: json - - { - 'abc': 123 - } - -.. rstcheck: ignore-language json,python,rst -""")) - - def test_check_xml(self): - self.assert_lines_equal( - [8], - rstcheck.check( - """\ -Test -==== - -.. code-block:: xml - - - - 123 - -""")) - - def test_check_xml_with_ignore(self): - self.assert_lines_equal( - [], - rstcheck.check( - """\ -Test -==== - -.. code-block:: xml - - - - 123 - - -.. rstcheck: ignore-language=xml,python,rst -""")) - - def test_check_xml_with_unmatched_ignores_only(self): - self.assert_lines_equal( - [8], - rstcheck.check( - """\ -Test -==== - -.. code-block:: xml - - - - 123 - - -.. rstcheck: ignore-language=cpp,python,rst -""")) - - def test_check_xml_with_bad_ignore(self): - self.assert_lines_equal( - [8, 11], - rstcheck.check( - """\ -Test -==== - -.. code-block:: xml - - - - 123 - - -.. rstcheck: ignore-language xml,python,rst -""")) - - def test_check_with_extra_blank_lines_before(self): - self.assert_lines_equal( - [8], - rstcheck.check( - """\ -Test -==== - -.. code-block:: python - - - - print( -""")) - - def test_check_with_extra_blank_lines_after(self): - self.assert_lines_equal( - [6], - rstcheck.check( - """\ -Test -==== - -.. code-block:: python - - print( - - - -""")) - - def test_check_with_extra_blank_lines_before_and_after(self): - self.assert_lines_equal( - [8], - rstcheck.check( - """\ -Test -==== - -.. code-block:: python - - - - print( - - - -""")) - - def test_check_rst(self): - self.assert_lines_equal( - [2], - rstcheck.check( - """\ -Test -=== -""")) - - def test_check_rst_report_level(self): - self.assert_lines_equal( - [], - rstcheck.check( - """\ -Test -=== -""", - report_level=5)) - - def test_check_nested_rst(self): - self.assert_lines_equal( - [32], - rstcheck.check( - """\ -Test -==== - -.. code-block:: rst - - Test - ==== - - .. code-block:: rst - - - Test - ==== - - .. code-block:: rst - - Test - ==== - - .. code-block:: rst - - Test - ==== - - .. code-block:: rst - - Test - ==== - - .. code-block:: python - - print( -""")) - - @unittest.skipIf(not rstcheck.SPHINX_INSTALLED, - 'Requires Sphinx') - def test_ignore_sphinx_directives(self): - self.assert_lines_equal( - [], - rstcheck.check( - """\ -.. toctree:: - :maxdepth: 2 - - intro - strings - datatypes - numeric - (many more documents listed here) - -.. highlight:: python - :linenothreshold: 5 - -:: - - print('Hello') - -.. code-block:: ruby - :linenos: - - puts "Hello!" - -.. code-block:: python - :linenos: - :emphasize-lines: 3,5 - - def some_function(): - interesting = False - print('This line is highlighted.') - print('This one is not...') - print('...but this one is.') - -.. literalinclude:: rstcheck.py - :language: python - :linenos: - -""")) - - def test_check_doctest(self): - self.assert_lines_equal( - [5], - rstcheck.check( - """\ -Testing -======= - ->>> x = 1 ->>>> x -1 -""")) - - def test_check_doctest_do_not_crash_when_indented(self): - """docutils does not provide line number when indented.""" - list(rstcheck.check( - """\ -Testing -======= - - >>> x = 1 - >>>> x - 1 -""")) - - def test_check_doctest_with_ignore(self): - self.assert_lines_equal( - [], - rstcheck.check( - """\ -Testing -======= - ->>> x = 1 ->>>> x -1 - -.. rstcheck: ignore-language=doctest -""")) - - @unittest.skipIf(rstcheck.SPHINX_INSTALLED, - 'Does not work with Sphinx') - def test_check_doctest_in_code(self): - self.assert_lines_equal( - [7], - rstcheck.check( - """\ -Testing -======= - -.. code:: doctest - - >>> x = 1 - >>>> x - 1 -""")) - - def test_check_doctest_in_code_block(self): - self.assert_lines_equal( - [7], - rstcheck.check( - """\ -Testing -======= - -.. code-block:: doctest - - >>> x = 1 - >>>> x - 1 -""")) - - def test_check_doctest_in_python_code_block(self): - """I'm not sure if this is correct, but I've seen people do it.""" - self.assert_lines_equal( - [7], - rstcheck.check( - """\ -Testing -======= - -.. code-block:: python - - >>> x = 1 - >>>> x - 1 -""")) - - -def main(): - with rstcheck.enable_sphinx_if_possible(): - unittest.main() - - -if __name__ == '__main__': - main() diff --git a/examples/bad/bad_bash.rst b/testing/examples/bad/bash.rst similarity index 52% rename from examples/bad/bad_bash.rst rename to testing/examples/bad/bash.rst index b0eaeb8e..49636700 100644 --- a/examples/bad/bad_bash.rst +++ b/testing/examples/bad/bash.rst @@ -2,6 +2,6 @@ Test ==== -.. code-block:: bash +.. code:: bash { diff --git a/examples/bad/bad_python.rst b/testing/examples/bad/code.rst similarity index 54% rename from examples/bad/bad_python.rst rename to testing/examples/bad/code.rst index 5e5855e8..b9d0bad7 100644 --- a/examples/bad/bad_python.rst +++ b/testing/examples/bad/code.rst @@ -2,6 +2,6 @@ Test ==== -.. code-block:: python +.. code:: python print( diff --git a/examples/bad/bad_cpp.rst b/testing/examples/bad/cpp.rst similarity index 75% rename from examples/bad/bad_cpp.rst rename to testing/examples/bad/cpp.rst index 6cf46649..8423a8fd 100644 --- a/examples/bad/bad_cpp.rst +++ b/testing/examples/bad/cpp.rst @@ -2,7 +2,7 @@ Test ==== -.. code-block:: cpp +.. code:: cpp int main() { diff --git a/examples/bad/bad_markdown.rst b/testing/examples/bad/markdown.rst similarity index 100% rename from examples/bad/bad_markdown.rst rename to testing/examples/bad/markdown.rst diff --git a/examples/bad/bad_code.rst b/testing/examples/bad/python.rst similarity index 54% rename from examples/bad/bad_code.rst rename to testing/examples/bad/python.rst index 4b37a3fd..b9d0bad7 100644 --- a/examples/bad/bad_code.rst +++ b/testing/examples/bad/python.rst @@ -2,8 +2,6 @@ Test ==== -``code`` rather than ``code-block``. - .. code:: python print( diff --git a/examples/bad/bad_rst.rst b/testing/examples/bad/rst.rst similarity index 100% rename from examples/bad/bad_rst.rst rename to testing/examples/bad/rst.rst diff --git a/examples/bad/bad_rst_in_rst.rst b/testing/examples/bad/rst_in_rst.rst similarity index 64% rename from examples/bad/bad_rst_in_rst.rst rename to testing/examples/bad/rst_in_rst.rst index 04171cb2..1844e2fb 100644 --- a/examples/bad/bad_rst_in_rst.rst +++ b/testing/examples/bad/rst_in_rst.rst @@ -2,7 +2,7 @@ Test ==== -.. code-block:: rst +.. code:: rst Testing === diff --git a/testing/examples/bad/table_substitutions.rst b/testing/examples/bad/table_substitutions.rst new file mode 100644 index 00000000..d68ce106 --- /dev/null +++ b/testing/examples/bad/table_substitutions.rst @@ -0,0 +1,37 @@ +Testing +======= + +.. |BAZ_ID| replace:: baz + +The tables below are properly formatted, but contain substitution references +that are not defined in the document. If those substitutions are not ignored +through configuration option, rstcheck should complain about the missing +definitions. If they *are* marked as ignored through command line or +configuration file, then everything should be good. + +In the past, rstcheck would fail to validate such tables when the user would +ignore the missing references. This was because substitution references were +simply replaced with "None", potentially leaving cells with a padding of an +incorrect size. + +First, let's try with a grid table. + ++------+------------+ +| Name | Identifier | ++======+============+ +| Foo | |FOO_ID| | ++------+------------+ +| Bar | |BAR_ID| | ++------+------------+ +| Baz | |BAZ_ID| | ++------+------------+ + +Take two: with a simple table. + +==== ========== +Name Identifier +==== ========== +Foo |FOO_ID| +Bar |BAR_ID| +Baz |BAZ_ID| +==== ========== diff --git a/testing/examples/custom/custom_directive_and_role.rst b/testing/examples/custom/custom_directive_and_role.rst new file mode 100644 index 00000000..9da2b1e3 --- /dev/null +++ b/testing/examples/custom/custom_directive_and_role.rst @@ -0,0 +1,4 @@ +.. custom-directive:: + + +:custom-role:`testing` diff --git a/testing/examples/custom/rstcheck.custom.ini b/testing/examples/custom/rstcheck.custom.ini new file mode 100644 index 00000000..562e724d --- /dev/null +++ b/testing/examples/custom/rstcheck.custom.ini @@ -0,0 +1,3 @@ +[rstcheck] +ignore_directives=custom-directive +ignore_roles=custom-role diff --git a/examples/good/bom.rst b/testing/examples/good/bom.rst similarity index 78% rename from examples/good/bom.rst rename to testing/examples/good/bom.rst index 34a03607..915a49f9 100644 --- a/examples/good/bom.rst +++ b/testing/examples/good/bom.rst @@ -4,4 +4,4 @@ docutils natively reports a false positive warning about the underline being to short if there is a BOM at the beginning of the title. -https://github.com/myint/rstcheck/issues/23 +https://github.com/rstcheck/rstcheck/issues/23 diff --git a/examples/good/good.rst b/testing/examples/good/code_blocks.rst similarity index 78% rename from examples/good/good.rst rename to testing/examples/good/code_blocks.rst index 554351f4..88bfce1d 100644 --- a/examples/good/good.rst +++ b/testing/examples/good/code_blocks.rst @@ -2,14 +2,14 @@ Test ==== -.. code-block:: bash +.. code:: bash if [ "$x" == 'y' ] then exit 1 fi -.. code-block:: c +.. code:: c float foo(int n) { @@ -19,7 +19,7 @@ Test return x[0]; } -.. code-block:: cpp +.. code:: cpp #include @@ -29,20 +29,20 @@ Test return x; } -.. code-block:: python +.. code:: python print(1) Run more tests for checking performance. -.. code-block:: bash +.. code:: bash if [ "$x" == 'y' ] then exit 1 fi -.. code-block:: c +.. code:: c float foo(int n) { @@ -52,7 +52,7 @@ Run more tests for checking performance. return x[0]; } -.. code-block:: cpp +.. code:: cpp #include @@ -62,18 +62,18 @@ Run more tests for checking performance. return x; } -.. code-block:: python +.. code:: python print(1) -.. code-block:: bash +.. code:: bash if [ "$x" == 'y' ] then exit 1 fi -.. code-block:: c +.. code:: c float foo(int n) { @@ -83,7 +83,7 @@ Run more tests for checking performance. return x[0]; } -.. code-block:: cpp +.. code:: cpp #include @@ -93,18 +93,18 @@ Run more tests for checking performance. return x; } -.. code-block:: python +.. code:: python print(1) -.. code-block:: bash +.. code:: bash if [ "$x" == 'y' ] then exit 1 fi -.. code-block:: c +.. code:: c float foo(int n) { @@ -114,7 +114,7 @@ Run more tests for checking performance. return x[0]; } -.. code-block:: cpp +.. code:: cpp #include @@ -124,18 +124,18 @@ Run more tests for checking performance. return x; } -.. code-block:: python +.. code:: python print(1) -.. code-block:: bash +.. code:: bash if [ "$x" == 'y' ] then exit 1 fi -.. code-block:: c +.. code:: c float foo(int n) { @@ -145,7 +145,7 @@ Run more tests for checking performance. return x[0]; } -.. code-block:: cpp +.. code:: cpp #include @@ -155,7 +155,7 @@ Run more tests for checking performance. return x; } -.. code-block:: python +.. code:: python # ¬∆˚ß∂ƒß∂ƒ˚¬∆ print(1) diff --git a/examples/good/good_cpp_with_local_include.rst b/testing/examples/good/cpp_with_local_include.rst similarity index 81% rename from examples/good/good_cpp_with_local_include.rst rename to testing/examples/good/cpp_with_local_include.rst index aeb4980a..96739ef7 100644 --- a/examples/good/good_cpp_with_local_include.rst +++ b/testing/examples/good/cpp_with_local_include.rst @@ -2,7 +2,7 @@ Test ==== -.. code-block:: cpp +.. code:: cpp #include "foo.h" diff --git a/examples/good/foo.h b/testing/examples/good/foo.h similarity index 100% rename from examples/good/foo.h rename to testing/examples/good/foo.h diff --git a/examples/good/good_markdown.rst b/testing/examples/good/markdown.rst similarity index 73% rename from examples/good/good_markdown.rst rename to testing/examples/good/markdown.rst index dbe0ff40..f8adb5fa 100644 --- a/examples/good/good_markdown.rst +++ b/testing/examples/good/markdown.rst @@ -2,6 +2,6 @@ Test ==== -.. code-block:: markdown +.. code:: markdown [Markdown-style Link](https://www.example.com/) diff --git a/testing/examples/good/rst.rst b/testing/examples/good/rst.rst new file mode 100644 index 00000000..f75d71cb --- /dev/null +++ b/testing/examples/good/rst.rst @@ -0,0 +1,3 @@ +==== +Test +==== diff --git a/examples/good/unicode.rst b/testing/examples/good/unicode.rst similarity index 62% rename from examples/good/unicode.rst rename to testing/examples/good/unicode.rst index 183e8ae5..4b32632e 100644 --- a/examples/good/unicode.rst +++ b/testing/examples/good/unicode.rst @@ -2,11 +2,11 @@ Тест ==== -.. code-block:: python +.. code:: python - print('Привет!') + print("Привет!") -.. code-block:: bash +.. code:: bash $ echo 'Привет' >> pipe.txt $ echo 'файловая труба!' >> pipe.txt diff --git a/testing/examples/inline_config/with_inline_ignore.rst b/testing/examples/inline_config/with_inline_ignore.rst new file mode 100644 index 00000000..4f63d4da --- /dev/null +++ b/testing/examples/inline_config/with_inline_ignore.rst @@ -0,0 +1,20 @@ +Inline ignore comment example +============================= + +This is a copy of ``without_inline_ignore.rst`` with ignore comments below. + +.. custom-directive:: + +:custom-role:`example` + +.. code:: python + + print( + +|unmatched-substitution| + + +.. rstcheck: ignore-directives=custom-directive +.. rstcheck: ignore-roles=custom-role +.. rstcheck: ignore-languages=python +.. rstcheck: ignore-substitutions=unmatched-substitution diff --git a/testing/examples/inline_config/with_inline_skip_code_block.rst b/testing/examples/inline_config/with_inline_skip_code_block.rst new file mode 100644 index 00000000..9dd0ecf0 --- /dev/null +++ b/testing/examples/inline_config/with_inline_skip_code_block.rst @@ -0,0 +1,9 @@ +.. code-block:: python + + print( + + +.. rstcheck: ignore-next-code-block +.. code-block:: python + + print( diff --git a/testing/examples/inline_config/with_nested_inline_skip_code_block.rst b/testing/examples/inline_config/with_nested_inline_skip_code_block.rst new file mode 100644 index 00000000..495fcbdd --- /dev/null +++ b/testing/examples/inline_config/with_nested_inline_skip_code_block.rst @@ -0,0 +1,13 @@ +.. code-block:: rst + + .. code-block:: python + + print( + + +.. code-block:: rst + + .. rstcheck: ignore-next-code-block + .. code-block:: python + + print( diff --git a/testing/examples/inline_config/without_inline_ignore.rst b/testing/examples/inline_config/without_inline_ignore.rst new file mode 100644 index 00000000..2a8a8754 --- /dev/null +++ b/testing/examples/inline_config/without_inline_ignore.rst @@ -0,0 +1,14 @@ +Inline ignore comment example +============================= + +This is a copy of ``with_inline_ignore.rst`` without ignore comments. + +.. custom-directive:: + +:custom-role:`example` + +.. code:: python + + print( + +|unmatched-substitution| diff --git a/examples/good/unknown.rst b/testing/examples/sphinx/good.rst similarity index 100% rename from examples/good/unknown.rst rename to testing/examples/sphinx/good.rst diff --git a/testing/examples/with_configuration/.rstcheck.cfg b/testing/examples/with_configuration/.rstcheck.cfg new file mode 100644 index 00000000..220d8e64 --- /dev/null +++ b/testing/examples/with_configuration/.rstcheck.cfg @@ -0,0 +1,6 @@ +[rstcheck] +ignore_directives=foobar,custom-directive +ignore_roles=custom-role +ignore_messages=(Title underline too short\.$) +ignore_languages=cpp +report_level=warning diff --git a/testing/examples/with_configuration/bad.rst b/testing/examples/with_configuration/bad.rst new file mode 100644 index 00000000..9915d5a8 --- /dev/null +++ b/testing/examples/with_configuration/bad.rst @@ -0,0 +1,14 @@ +Subtitle +------- + +.. custom-directive:: + + +:custom-role:`testing` + +.. code:: cpp + + int main() + { + return x; + } diff --git a/testing/examples/with_configuration/bad_config.cfg b/testing/examples/with_configuration/bad_config.cfg new file mode 100644 index 00000000..637728e2 --- /dev/null +++ b/testing/examples/with_configuration/bad_config.cfg @@ -0,0 +1,4 @@ +[rstcheck] +ignore_directives=foobar,custom-directive +report=warning +unknown=true diff --git a/testing/examples/with_configuration/bad_config.toml b/testing/examples/with_configuration/bad_config.toml new file mode 100644 index 00000000..cd936958 --- /dev/null +++ b/testing/examples/with_configuration/bad_config.toml @@ -0,0 +1,4 @@ +[tool.rstcheck] +ignore_directives = ["foobar", "custom-directive"] +report = "warning" +unknown = true diff --git a/examples/with_configuration/bad-2.rst b/testing/examples/with_configuration/bad_rst.rst similarity index 81% rename from examples/with_configuration/bad-2.rst rename to testing/examples/with_configuration/bad_rst.rst index 0d5455c0..1537d288 100644 --- a/examples/with_configuration/bad-2.rst +++ b/testing/examples/with_configuration/bad_rst.rst @@ -4,4 +4,4 @@ Duplicate ========= Subtitle --------- +------- diff --git a/testing/examples/with_configuration/dummydir/.gitkeep b/testing/examples/with_configuration/dummydir/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/testing/examples/with_configuration/pyproject.toml b/testing/examples/with_configuration/pyproject.toml new file mode 100644 index 00000000..ba6bf226 --- /dev/null +++ b/testing/examples/with_configuration/pyproject.toml @@ -0,0 +1,15 @@ +[tool.rstcheck] +ignore_directives = [ + "foobar", + "custom-directive" +] +ignore_roles = [ + "custom-role" +] +ignore_messages = [ + "(Title underline too short\\.$)" +] +ignore_languages = [ + "cpp" +] +report_level = "warning" diff --git a/testing/examples/with_configuration/rstcheck.ini b/testing/examples/with_configuration/rstcheck.ini new file mode 100644 index 00000000..3d59b981 --- /dev/null +++ b/testing/examples/with_configuration/rstcheck.ini @@ -0,0 +1,7 @@ +[rstcheck] +# Duplicate of .rstcheck.cfg +ignore_directives=foobar,custom-directive +ignore_roles=custom-role +ignore_messages=(Title underline too short\.$) +ignore_languages=cpp +report_level=warning diff --git a/testing/examples/without_configuration/bad.rst b/testing/examples/without_configuration/bad.rst new file mode 100644 index 00000000..fd1a7a33 --- /dev/null +++ b/testing/examples/without_configuration/bad.rst @@ -0,0 +1,16 @@ +.. copy of with_configuration/bad.rst + +Subtitle +------- + +.. custom-directive:: + + +:custom-role:`testing` + +.. code:: cpp + + int main() + { + return x; + } diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/_cli_test.py b/tests/_cli_test.py new file mode 100644 index 00000000..4600fa1b --- /dev/null +++ b/tests/_cli_test.py @@ -0,0 +1,24 @@ +"""Tests for ``_cli`` module. + +``_cli.cli`` is not really good testable with unit tests. +Therefore tests are in the ``integration_tests`` directory. +""" + +from __future__ import annotations + +import pytest + +from rstcheck import _cli + + +@pytest.mark.parametrize("level", ["SEVERE", "NONE"]) +def test_setup_logger_errors_on_invalid_levels(level: str) -> None: + """Tests exception is raised on invalid levels.""" + with pytest.raises(TypeError, match=f"Invalid log level: {level}"): + _cli.setup_logger(level) + + +@pytest.mark.parametrize("level", ["debug", "InFO", "WARNING", "ErRor", "critical"]) +def test_setup_logger_valid_levels(level: str) -> None: + """Test no exception is raised on valid levels.""" + _cli.setup_logger(level) # act diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..d8dcb7b4 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +"""Fixtures for tests.""" + +from __future__ import annotations + +import pathlib + +REPO_DIR = pathlib.Path(__file__).resolve().parents[1].resolve() +TESTING_DIR = REPO_DIR / "testing" +EXAMPLES_DIR = TESTING_DIR / "examples" diff --git a/tests/integration_tests/__init__.py b/tests/integration_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration_tests/cli_test.py b/tests/integration_tests/cli_test.py new file mode 100644 index 00000000..c2da7bc1 --- /dev/null +++ b/tests/integration_tests/cli_test.py @@ -0,0 +1,657 @@ +"""Integration test for the CLI.""" + +from __future__ import annotations + +import pathlib +import re +import sys + +import pytest +import typer +import typer.testing +from rstcheck_core import _extras + +from tests.conftest import EXAMPLES_DIR, TESTING_DIR +from tests.integration_tests.conftest import ERROR_CODE_REGEX + + +def test_exit_0_on_nonexisting_config_path( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner, caplog: pytest.LogCaptureFixture +) -> None: + """Test runner exits with error on non existing config path.""" + test_file = EXAMPLES_DIR / "good" / "rst.rst" + config_file = pathlib.Path("does-not-exist") + + result = cli_runner.invoke(cli_app, [str(test_file), "--config", str(config_file)]) + + assert result.exit_code != 0 + assert "Passed config path was not found" in caplog.text + + +class TestHelpMessage: + """Test help CLI messages.""" + + @staticmethod + def test_cli_version_message(cli_app: typer.Typer, cli_runner: typer.testing.CliRunner) -> None: + """Test help message.""" + result = cli_runner.invoke(cli_app, "--version") + + assert result.exit_code == 0 + assert "rstcheck CLI Version:" in result.stdout + assert "rstcheck-core Version:" in result.stdout + + @staticmethod + def test_cli_help_message(cli_app: typer.Typer, cli_runner: typer.testing.CliRunner) -> None: + """Test help message.""" + result = cli_runner.invoke(cli_app, "--help") + + assert result.exit_code == 0 + assert "Enabled features:" in result.stdout + + @staticmethod + @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") + def test_cli_help_message_with_sphinx( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test help message when sphinx is installed.""" + result = cli_runner.invoke(cli_app, "--help") + + assert result.exit_code == 0 + assert "Enabled features:" in result.stdout + assert "Sphinx" in result.stdout + + @staticmethod + @pytest.mark.skipif(not _extras.TOMLI_INSTALLED, reason="Depends on toml extra.") + def test_cli_help_message_with_tomli( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test help message when toml is installed.""" + result = cli_runner.invoke(cli_app, "--help") + + assert result.exit_code == 0 + assert "pyproject.toml" in result.stdout + assert "Enabled features:" in result.stdout + assert "Toml" in result.stdout + + +class TestInput: + """Test file input with good and bad files and piping.""" + + @staticmethod + @pytest.mark.xfail( + sys.platform == "win32", reason="Unknown Windows specific wrong result", strict=True + ) + @pytest.mark.parametrize("test_file", list(TESTING_DIR.glob("examples/good/*.rst"))) + def test_all_good_examples( + test_file: pathlib.Path, + cli_app: typer.Typer, + cli_runner: typer.testing.CliRunner, + ) -> None: + """Test all files in ``testing/examples/good`` are errorless.""" + result = cli_runner.invoke(cli_app, str(test_file)) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + @staticmethod + @pytest.mark.xfail( + sys.platform == "win32", reason="Random unknown Windows specific wrong result", strict=False + ) + def test_all_good_examples_recurively( + cli_app: typer.Typer, + cli_runner: typer.testing.CliRunner, + ) -> None: + """Test all files in ``testing/examples/good`` recursively.""" + test_dir = EXAMPLES_DIR / "good" + + result = cli_runner.invoke(cli_app, [str(test_dir), "--recursive"]) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + @staticmethod + @pytest.mark.xfail( + sys.platform == "win32", reason="Unknown Windows specific wrong result", strict=True + ) + @pytest.mark.parametrize("test_file", list(TESTING_DIR.glob("examples/bad/*.rst"))) + def test_all_bad_examples( + test_file: pathlib.Path, + cli_app: typer.Typer, + cli_runner: typer.testing.CliRunner, + ) -> None: + """Test all files in ``testing/examples/bad`` have errors.""" + result = cli_runner.invoke(cli_app, str(test_file)) + + assert result.exit_code != 0 + assert ERROR_CODE_REGEX.search(result.stdout) is not None + + @staticmethod + def test_all_bad_examples_recurively( + cli_app: typer.Typer, + cli_runner: typer.testing.CliRunner, + ) -> None: + """Test all files in ``testing/examples/bad`` recursively.""" + test_dir = EXAMPLES_DIR / "bad" + + result = cli_runner.invoke(cli_app, [str(test_dir), "--recursive"]) + + assert result.exit_code != 0 + assert ERROR_CODE_REGEX.search(result.stdout) is not None + + @staticmethod + def test_mix_of_good_and_bad_examples( + cli_app: typer.Typer, + cli_runner: typer.testing.CliRunner, + ) -> None: + """Test mix of good and bad examples.""" + test_file_good = EXAMPLES_DIR / "good" / "rst.rst" + test_file_bad = EXAMPLES_DIR / "bad" / "rst.rst" + + result = cli_runner.invoke(cli_app, [str(test_file_good), str(test_file_bad)]) + + assert result.exit_code != 0 + assert len(ERROR_CODE_REGEX.findall(result.stdout)) == 1 + + @staticmethod + def test_good_example_with_piping( + cli_app: typer.Typer, + cli_runner: typer.testing.CliRunner, + ) -> None: + """Test good example file piped into rstcheck.""" + test_file = EXAMPLES_DIR / "good" / "rst.rst" + test_file_content = test_file.read_text("utf-8") + + result = cli_runner.invoke(cli_app, "-", input=test_file_content) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + @staticmethod + def test_bad_example_with_piping( + cli_app: typer.Typer, + cli_runner: typer.testing.CliRunner, + ) -> None: + """Test bad example file piped into rstcheck.""" + test_file = EXAMPLES_DIR / "bad" / "rst.rst" + test_file_content = test_file.read_text("utf-8") + + result = cli_runner.invoke(cli_app, "-", input=test_file_content) + + assert result.exit_code != 0 + assert len(ERROR_CODE_REGEX.findall(result.stdout)) == 1 + + @staticmethod + def test_piping_is_not_allowed_with_additional_files(cli_app: typer.Typer) -> None: + """Test piping into rstcheck is not allowed with additional files. + + Test cli prints error to stderr. + """ + cli_runner_divided_output = typer.testing.CliRunner(mix_stderr=False) + + result = cli_runner_divided_output.invoke(cli_app, ["-", "foo"]) + + assert result.exit_code == 1 + assert "'-' is only allowed without additional files." in result.stderr + assert "Aborted" in result.stderr + + +class TestIgnoreOptions: + """Test --ignore-* options and --report-level.""" + + @staticmethod + def test_without_report_exits_zero( + cli_app: typer.Typer, + cli_runner: typer.testing.CliRunner, + ) -> None: + """Test bad example without report is ok.""" + test_file = EXAMPLES_DIR / "bad" / "rst.rst" + + result = cli_runner.invoke(cli_app, [str(test_file), "--report-level", "none"]) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + @staticmethod + def test_ignore_language_silences_error( + cli_app: typer.Typer, + cli_runner: typer.testing.CliRunner, + ) -> None: + """Test bad example with ignored language is ok.""" + test_file = EXAMPLES_DIR / "bad" / "cpp.rst" + + result = cli_runner.invoke(cli_app, [str(test_file), "--ignore-languages", "cpp"]) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + @staticmethod + def test_matching_ignore_msg_exits_zero( + cli_app: typer.Typer, + cli_runner: typer.testing.CliRunner, + ) -> None: + """Test matching ignore message.""" + test_file = EXAMPLES_DIR / "bad" / "rst.rst" + + result = cli_runner.invoke( + cli_app, + [str(test_file), "--ignore-messages", r"(Title .verline & underline mismatch\.$)"], + ) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + @staticmethod + def test_non_matching_ignore_msg_errors( + cli_app: typer.Typer, + cli_runner: typer.testing.CliRunner, + ) -> None: + """Test non matching ignore message.""" + test_file = EXAMPLES_DIR / "bad" / "rst.rst" + + result = cli_runner.invoke(cli_app, [str(test_file), "--ignore-messages", r"(No match\.$)"]) + + assert result.exit_code != 0 + assert "Error! Issues detected." in result.stdout + + @staticmethod + def test_table_substitution_error_fixed_by_ignore( + cli_app: typer.Typer, + cli_runner: typer.testing.CliRunner, + ) -> None: + """Test that ignored substitutions in tables are correctly handled.""" + test_file = EXAMPLES_DIR / "bad" / "table_substitutions.rst" + + result = cli_runner.invoke( + cli_app, [str(test_file), "--ignore-substitutions", "FOO_ID,BAR_ID"] + ) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + +class TestWithoutConfigFile: + """Test without config file in dir tree.""" + + @staticmethod + @pytest.mark.xfail( + sys.platform == "win32", reason="Unknown Windows specific wrong result", strict=True + ) + def test_error_without_config_file( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test bad example without set config file and implicit config file shows errors.""" + test_file = EXAMPLES_DIR / "without_configuration" / "bad.rst" + + result = cli_runner.invoke(cli_app, str(test_file)) + + assert result.exit_code != 0 + assert len(ERROR_CODE_REGEX.findall(result.stdout)) == 6 + + @staticmethod + def test_no_error_with_set_ini_config_file( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test bad example with set INI config file does not error.""" + test_file = EXAMPLES_DIR / "without_configuration" / "bad.rst" + config_file = EXAMPLES_DIR / "with_configuration" / "rstcheck.ini" + + result = cli_runner.invoke(cli_app, [str(test_file), "--config", str(config_file)]) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + @staticmethod + def test_no_error_with_set_config_dir( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test bad example with set config dir does not error.""" + test_file = EXAMPLES_DIR / "without_configuration" / "bad.rst" + config_dir = EXAMPLES_DIR / "with_configuration" + + result = cli_runner.invoke(cli_app, [str(test_file), "--config", str(config_dir)]) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + @staticmethod + @pytest.mark.skipif(not _extras.TOMLI_INSTALLED, reason="Depends on toml extra.") + def test_no_error_with_set_toml_config_file( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test bad example with set TOML config file does not error.""" + test_file = EXAMPLES_DIR / "without_configuration" / "bad.rst" + config_file = EXAMPLES_DIR / "with_configuration" / "pyproject.toml" + + result = cli_runner.invoke(cli_app, [str(test_file), "--config", str(config_file)]) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + +class TestWithConfigFile: + """Test with config file in dir tree.""" + + @staticmethod + def test_file_1_is_bad_without_config( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test bad file ``bad.rst`` without config file is not ok.""" + test_file = EXAMPLES_DIR / "with_configuration" / "bad.rst" + config_file = pathlib.Path("NONE") + + result = cli_runner.invoke(cli_app, [str(test_file), "--config", str(config_file)]) + + assert result.exit_code != 0 + assert len(ERROR_CODE_REGEX.findall(result.stdout)) == 6 + + @staticmethod + def test_file_2_is_bad_without_config( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test bad file ``bad_rst.rst`` without config file not ok.""" + test_file = EXAMPLES_DIR / "with_configuration" / "bad_rst.rst" + config_file = pathlib.Path("NONE") + + result = cli_runner.invoke(cli_app, [str(test_file), "--config", str(config_file)]) + + assert result.exit_code != 0 + assert len(ERROR_CODE_REGEX.findall(result.stdout)) == 2 + + @staticmethod + @pytest.mark.xfail( + sys.platform == "win32", reason="Unknown Windows specific wrong result", strict=True + ) + def test_bad_file_1_with_implicit_config_no_errors( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test bad file ``bad.rst`` with implicit config file is ok.""" + test_file = EXAMPLES_DIR / "with_configuration" / "bad.rst" + + result = cli_runner.invoke(cli_app, str(test_file)) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + @staticmethod + @pytest.mark.xfail( + sys.platform == "win32", reason="Unknown Windows specific wrong result", strict=True + ) + def test_bad_file_2_with_implicit_config_some_errors( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test bad file ``bad_rst.rst`` with implicit config file partially ok.""" + test_file = EXAMPLES_DIR / "with_configuration" / "bad_rst.rst" + + result = cli_runner.invoke(cli_app, str(test_file)) + + assert result.exit_code != 0 + assert len(ERROR_CODE_REGEX.findall(result.stdout)) == 1 + + @staticmethod + def test_bad_file_1_with_explicit_config_no_errors( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test bad file ``bad.rst`` with explicit config file is ok.""" + test_file = EXAMPLES_DIR / "with_configuration" / "bad.rst" + config_file = EXAMPLES_DIR / "with_configuration" / "rstcheck.ini" + + result = cli_runner.invoke(cli_app, [str(test_file), "--config", str(config_file)]) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + @staticmethod + def test_bad_file_2_with_explicit_config_some_errors( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test bad file ``bad_rst.rst`` with explicit config file partially ok.""" + test_file = EXAMPLES_DIR / "with_configuration" / "bad_rst.rst" + config_file = EXAMPLES_DIR / "with_configuration" / "rstcheck.ini" + + result = cli_runner.invoke(cli_app, [str(test_file), "--config", str(config_file)]) + + assert result.exit_code != 0 + assert len(ERROR_CODE_REGEX.findall(result.stdout)) == 1 + + +class TestWarningOnUnknownSettings: + """Test warnings logged on unknown settings in config files.""" + + @staticmethod + @pytest.mark.parametrize("config_file_name", ["bad_config.cfg", "bad_config.toml"]) + def test_no_warnings_are_logged_by_default( + config_file_name: str, + cli_app: typer.Typer, + cli_runner: typer.testing.CliRunner, + caplog: pytest.LogCaptureFixture, + ) -> None: + """Test that no warning is logged on unknown setting by default.""" + test_file = EXAMPLES_DIR / "good" / "rst.rst" + config_file = EXAMPLES_DIR / "with_configuration" / config_file_name + + result = cli_runner.invoke(cli_app, [str(test_file), "--config", str(config_file)]) + + assert result.exit_code == 0 + assert "Unknown setting(s)" not in caplog.text + + @staticmethod + @pytest.mark.parametrize("config_file_name", ["bad_config.cfg", "bad_config.toml"]) + def test_warnings_are_logged_when_set( + config_file_name: str, + cli_app: typer.Typer, + cli_runner: typer.testing.CliRunner, + caplog: pytest.LogCaptureFixture, + ) -> None: + """Test that a warning is logged on unknown setting when activated.""" + test_file = EXAMPLES_DIR / "good" / "rst.rst" + config_file = EXAMPLES_DIR / "with_configuration" / config_file_name + + result = cli_runner.invoke( + cli_app, [str(test_file), "--config", str(config_file), "--warn-unknown-settings"] + ) + + assert result.exit_code == 0 + assert "Unknown setting(s)" in caplog.text + + +class TestCustomDirectivesAndRoles: + """Test custom directives and roles.""" + + @staticmethod + @pytest.mark.xfail( + sys.platform == "win32", reason="Unknown Windows specific wrong result", strict=True + ) + def test_custom_directive_and_role( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test file with custom directive and role.""" + test_file = EXAMPLES_DIR / "custom" / "custom_directive_and_role.rst" + + result = cli_runner.invoke(cli_app, str(test_file)) + + assert result.exit_code != 0 + assert len(ERROR_CODE_REGEX.findall(result.stdout)) == 4 + + @staticmethod + def test_custom_directive_and_role_with_ignore( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test file with custom directive and role and CLI ignores.""" + test_file = EXAMPLES_DIR / "custom" / "custom_directive_and_role.rst" + + result = cli_runner.invoke( + cli_app, + [ + "--ignore-directives", + "custom-directive", + "--ignore-roles", + "custom-role", + str(test_file), + ], + ) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + @staticmethod + def test_custom_directive_and_role_with_config_file( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test file with custom directive and role and config file.""" + test_file = EXAMPLES_DIR / "custom" / "custom_directive_and_role.rst" + config_file = EXAMPLES_DIR / "custom" / "rstcheck.custom.ini" + + result = cli_runner.invoke(cli_app, ["--config", str(config_file), str(test_file)]) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + +class TestSphinx: + """Test integration with sphinx.""" + + @staticmethod + @pytest.mark.skipif(_extras.SPHINX_INSTALLED, reason="Test without sphinx extra.") + def test_sphinx_role_erros_without_sphinx( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test sphinx example errors without sphinx.""" + test_file = EXAMPLES_DIR / "sphinx" / "good.rst" + + result = cli_runner.invoke(cli_app, str(test_file)) + + assert result.exit_code != 0 + assert "Error! Issues detected." in result.stdout + + @staticmethod + @pytest.mark.xfail( + sys.platform == "win32", reason="Unknown Windows specific wrong result", strict=True + ) + @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") + def test_sphinx_role_exits_zero_with_sphinx( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test sphinx example does not error with sphinx.""" + test_file = EXAMPLES_DIR / "sphinx" / "good.rst" + + result = cli_runner.invoke(cli_app, str(test_file)) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + +class TestInlineIgnoreComments: + """Test inline config comments to ignore things.""" + + @staticmethod + @pytest.mark.xfail( + sys.platform == "win32", reason="Unknown Windows specific wrong result", strict=True + ) + def test_bad_example_has_issues( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test all issues are found on bad example.""" + test_file = EXAMPLES_DIR / "inline_config" / "without_inline_ignore.rst" + + result = cli_runner.invoke(cli_app, str(test_file)) + + assert result.exit_code != 0 + assert "custom-directive" in result.stdout + assert "custom-role" in result.stdout + assert "python" in result.stdout + assert "unmatched-substitution" in result.stdout + + @staticmethod + @pytest.mark.xfail( + sys.platform == "win32", reason="Unknown Windows specific wrong result", strict=True + ) + def test_bad_example_has_no_issues_with_inline_ignores( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test no issues are found on bad example with ignore comments.""" + test_file = EXAMPLES_DIR / "inline_config" / "with_inline_ignore.rst" + + result = cli_runner.invoke(cli_app, str(test_file)) + + assert result.exit_code == 0 + assert "Success! No issues detected." in result.stdout + + +class TestInlineFlowControlComments: + """Test inline flow control comments to e.g. skip things.""" + + @staticmethod + @pytest.mark.xfail( + sys.platform == "win32", reason="Unknown Windows specific wrong result", strict=True + ) + @pytest.mark.skipif(sys.version_info[0:2] > (3, 9), reason="Requires python3.9 or lower") + def test_bad_example_has_only_one_issue_pre310( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test only one issue is detected for two same code-blocks. + + One code-block has skip comment. + """ + test_file = EXAMPLES_DIR / "inline_config" / "with_inline_skip_code_block.rst" + + result = cli_runner.invoke(cli_app, str(test_file)) + + assert result.exit_code != 0 + assert len(re.findall(r"unexpected EOF while parsing", result.stdout)) == 1 + + @staticmethod + @pytest.mark.xfail( + sys.platform == "win32", reason="Unknown Windows specific wrong result", strict=True + ) + @pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires python3.10 or higher") + def test_bad_example_has_only_one_issue( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test only one issue is detected for two same code-blocks. + + One code-block has skip comment. + """ + test_file = EXAMPLES_DIR / "inline_config" / "with_inline_skip_code_block.rst" + + result = cli_runner.invoke(cli_app, str(test_file)) + + assert result.exit_code != 0 + assert len(re.findall(r"'\(' was never closed", result.stdout)) == 1 + + @staticmethod + @pytest.mark.xfail( + sys.platform == "win32", reason="Unknown Windows specific wrong result", strict=True + ) + @pytest.mark.skipif(sys.version_info[0:2] > (3, 9), reason="Requires python3.9 or lower") + def test_nested_bad_example_has_only_one_issue_pre310( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test only one issue is detected for two same nested code-blocks. + + One code-block has skip comment. + """ + test_file = EXAMPLES_DIR / "inline_config" / "with_nested_inline_skip_code_block.rst" + + result = cli_runner.invoke(cli_app, str(test_file)) + + assert result.exit_code != 0 + assert len(re.findall(r"unexpected EOF while parsing", result.stdout)) == 1 + + @staticmethod + @pytest.mark.xfail( + sys.platform == "win32", reason="Unknown Windows specific wrong result", strict=True + ) + @pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires python3.10 or higher") + def test_nested_bad_example_has_only_one_issue( + cli_app: typer.Typer, cli_runner: typer.testing.CliRunner + ) -> None: + """Test only one issue is detected for two same nested code-blocks. + + One code-block has skip comment. + """ + test_file = EXAMPLES_DIR / "inline_config" / "with_nested_inline_skip_code_block.rst" + + result = cli_runner.invoke(cli_app, str(test_file)) + + assert result.exit_code != 0 + assert len(re.findall(r"'\(' was never closed", result.stdout)) == 1 diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py new file mode 100644 index 00000000..ddb7684d --- /dev/null +++ b/tests/integration_tests/conftest.py @@ -0,0 +1,25 @@ +"""Fixtures for integration tests.""" + +from __future__ import annotations + +import re + +import pytest +import typer +import typer.testing + +from rstcheck import _cli + +ERROR_CODE_REGEX = re.compile(r"\([A-Z]*?/\d\)") + + +@pytest.fixture(name="cli_app") +def cli_app_fixture() -> typer.Typer: + """Create typer app from ``cli`` function for testing.""" + return _cli.app + + +@pytest.fixture(name="cli_runner") +def cli_runner_fixture() -> typer.testing.CliRunner: + """Create CLI Test Runner.""" + return typer.testing.CliRunner(mix_stderr=True) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..290bc0e6 --- /dev/null +++ b/tox.ini @@ -0,0 +1,237 @@ +[tox] +package = rstcheck_core + +minversion = 4 +skip_missing_interpreters = true +ignore_base_python_conflict = true +no_package = false + +env_list = + package + pre-commit-run + py{312,311,310,39,38} + py{312,311,310,39,38}-with-sphinx{5,6,7} + py{312,311,310,39,38}-with-tomli + coverage-all + docs-test-{html,linkcheck,doctest,spelling} + +labels = + test = py{312,311,310,39,38},py{312,311,310,39,38}-with-sphinx{5,6,7},py{312,311,310,39,38}-with-tomli,coverage-all + py3.8 = py38,py38-with-sphinx{5,6,7},py38-with-tomli,coverage-all + py3.9 = py39,py39-with-sphinx{5,6,7},py39-with-tomli,coverage-all + py3.10 = py310,py310-with-sphinx{5,6,7},py310-with-tomli,package,coverage-all + py3.11 = py311,py311-with-sphinx{5,6,7},py311-with-tomli,package,coverage-all + py3.12 = py312,py312-with-sphinx{5,6,7},py312-with-tomli,package,coverage-all + docs = docs-test-{html,doctest} + docs-full = docs-test-{html,linkcheck,doctest,spelling} + + +[testenv] +description = basic config env +base_python = python3.12 +pass_env = + HOME + CI + CI_FORCE_COLORS_PRE_COMMIT + CI_FORCE_COLORS_PYTEST + CI_FORCE_COLORS_SPHINX +set_env = + PIP_DISABLE_VERSION_CHECK = 1 +download = true +commands = + echo "Please use available tox envs" + exit 1 + + +[testenv:pre-commit] +description = format and check the code +env_dir = {toxworkdir}/pre-commit +pass_env = + {[testenv]pass_env} + SSH_AUTH_SOCK + SKIP +set_env = + {[testenv]set_env} + SKIP = {tty:identity:},{env:SKIP:} +skip_install = true +deps = pre-commit >= 2.17 +commands = + pre-commit {posargs:--help} + + +[testenv:pre-commit-run] +description = install pre-commit as git hook from the tox env +env_dir = {[testenv:pre-commit]env_dir} +skip_install = {[testenv:pre-commit]skip_install} +deps = {[testenv:pre-commit]deps} +commands = + pre-commit run {posargs} \ + --all-files \ + {tty::--show-diff-on-failure} \ + {tty:--color always:{env:CI_FORCE_COLORS_PRE_COMMIT:}} + + +[testenv:mypy] +description = run mypy type checker +extras = + toml + type_check + docs + testing +ignore_errors = true +commands = + mypy \ + --config-file {toxinidir}/pyproject.toml \ + src/rstcheck \ + tests \ + docs/source/conf.py \ + prep_release.py + + +[testenv:package] +description = check sdist and wheel +skip_install = true +deps = + build >= 1 + twine >= 3.3 +commands = + python -m build + twine check --strict dist/* + + +[testenv:py{312,311,310,39,38}] +description = run tests with {basepython} +pass_env = + {[testenv]pass_env} + PYTEST_* +set_env = + {[testenv]set_env} + COVERAGE_FILE = {env:COVERAGE_FILE:{toxinidir}/.coverage_cache/.coverage.{envname}} +extras = + testing +commands = + pytest \ + {tty:--color yes:{env:CI_FORCE_COLORS_PYTEST:}} \ + --basetemp {envtmpdir} \ + --cov {envsitepackagesdir}/{[tox]package} \ + --cov-fail-under 0 \ + {tty::-vvv} \ + {posargs:tests} + + +[testenv:py{312,311,310,39,38}-with-sphinx{5,6,7}] +description = run tests with {basepython} and sphinx +pass_env = + {[testenv]pass_env} + PYTEST_* +set_env = + {[testenv]set_env} + COVERAGE_FILE = {env:COVERAGE_FILE:{toxinidir}/.coverage_cache/.coverage.{envname}} +extras = + testing +deps = + sphinx5: sphinx>=5,<6 + sphinx6: sphinx>=6,<7 + sphinx7: sphinx>=7,<8 +commands = + pytest \ + {tty:--color yes:{env:CI_FORCE_COLORS_PYTEST:}} \ + --basetemp {envtmpdir} \ + --cov {envsitepackagesdir}/{[tox]package} \ + --cov-fail-under 0 \ + {tty::-vvv} \ + {posargs:tests} + + +[testenv:py{312,311,310,39,38}-with-tomli] +description = run tests with {basepython} and toml +pass_env = + {[testenv]pass_env} + PYTEST_* +set_env = + {[testenv]set_env} + COVERAGE_FILE = {env:COVERAGE_FILE:{toxinidir}/.coverage_cache/.coverage.{envname}} +extras = + toml + testing +commands = + pytest \ + {tty:--color yes:{env:CI_FORCE_COLORS_PYTEST:}} \ + --basetemp {envtmpdir} \ + --cov {envsitepackagesdir}/{[tox]package} \ + --cov-fail-under 0 \ + {tty::-vvv} \ + {posargs:tests} + + +[testenv:coverage-{all,merge,report}] +description = + all,merge: combine coverage data and create xml/html reports; + all,report: report total and diff coverage against origin/main (or DIFF_AGAINST) +env_dir = {toxworkdir}/coverage +depends = py{py3,312,311,310,39,38} +pass_env = + {[testenv]pass_env} + all,report: MIN_COVERAGE + all,report: MIN_DIFF_COVERAGE + all,report: DIFF_AGAINST + all,report: DIFF_RANGE_NOTATION +set_env = + {[testenv]set_env} + COVERAGE_FILE={toxinidir}/.coverage_cache/.coverage +skip_install = true +parallel_show_output = true +ignore_errors = true +deps = + diff-cover + coverage[toml] >=6.0 + coverage-conditional-plugin >=0.5 +commands = + all,merge: coverage combine + all,merge: coverage xml -o {toxinidir}/.coverage_cache/coverage.xml + all,merge: coverage html -d {toxinidir}/.coverage_cache/htmlcov + all,report: coverage report -m --fail-under {env:MIN_COVERAGE:0} + all,report: diff-cover --compare-branch {env:DIFF_AGAINST:origin/main} \ + all,report: --ignore-staged --ignore-unstaged \ + all,report: --fail-under {env:MIN_DIFF_COVERAGE:0} \ + all,report: --diff-range-notation {env:DIFF_RANGE_NOTATION:..} \ + all,report: {toxinidir}/.coverage_cache/coverage.xml + + +[testenv:docs{,-auto}] +description = build docs with sphinx +env_dir = {toxworkdir}/docs +set_env = + {[testenv]set_env} + TOXENV_BUILDCMD = sphinx-build {tty:--color:} + auto: TOXENV_BUILDCMD = sphinx-autobuild --re-ignore autoapidoc --watch {toxinidir}/src +extras = + toml + docs +commands = + # Build fresh docs + {env:TOXENV_BUILDCMD} -b html -aE docs/source docs/build/html + # Output link to index.html + python -c \ + 'from pathlib import Path; \ + index_file = Path(r"{toxinidir}")/"docs/build/html/index.html"; \ + print(f"DOCUMENTATION AVAILABLE UNDER: \{index_file.as_uri()\}")' + + +[testenv:docs-test-{html,linkcheck,doctest,spelling}] +description = Build and check docs with (see env name) sphinx builder +env_dir = {[testenv:docs]env_dir} +set_env = + {[testenv]set_env} + html: TOXENV_BUILDER = html + linkcheck: TOXENV_BUILDER = linkcheck + doctest: TOXENV_BUILDER = doctest + spelling: TOXENV_BUILDER = spelling + spelling: SPHINX_SPELLING = true +extras = {[testenv:docs]extras} +commands = + sphinx-build \ + {tty:--color:{env:CI_FORCE_COLORS_SPHINX:}} \ + -b {env:TOXENV_BUILDER} \ + -aE -v -nW --keep-going \ + docs/source docs/build/test/{env:TOXENV_BUILDER}