pax_global_header00006660000000000000000000000064145775176720014537gustar00rootroot0000000000000052 comment=30c6c46f65b7618004d279f53c5aa73bd877b2c3 rstcheck-core-1.2.1/000077500000000000000000000000001457751767200142745ustar00rootroot00000000000000rstcheck-core-1.2.1/.github/000077500000000000000000000000001457751767200156345ustar00rootroot00000000000000rstcheck-core-1.2.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001457751767200200175ustar00rootroot00000000000000rstcheck-core-1.2.1/.github/ISSUE_TEMPLATE/bug-report-lib.yaml000066400000000000000000000071551457751767200235450ustar00rootroot00000000000000--- 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 version are you using? You can find the rstcheck version with: Python >= 3.8: ```shell python -c "import importlib.metadata; print(importlib.metadata.version('rstcheck-core'))" ``` Python < 3.8: ```shell python -c "import importlib_metadata; print(importlib_metadata.version('rstcheck-core'))" ``` 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. rstcheck-core-1.2.1/.github/ISSUE_TEMPLATE/bug-report-tool.yaml000066400000000000000000000071511457751767200237500ustar00rootroot00000000000000--- 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 version are you using? You can find the rstcheck version with: Python >= 3.8: ```shell python -c "import importlib.metadata; print(importlib.metadata.version('rstcheck-core'))" ``` Python < 3.8: ```shell python -c "import importlib_metadata; print(importlib_metadata.version('rstcheck-core'))" ``` 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. rstcheck-core-1.2.1/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000000401457751767200220010ustar00rootroot00000000000000--- blank_issues_enabled: false rstcheck-core-1.2.1/.github/ISSUE_TEMPLATE/feature-request.yaml000066400000000000000000000047071457751767200240340ustar00rootroot00000000000000--- 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 version are you using? You can find the rstcheck version with: Python >= 3.8: ```shell python -c "import importlib.metadata; print(importlib.metadata.version('rstcheck-core'))" ``` Python < 3.8: ```shell python -c "import importlib_metadata; print(importlib_metadata.version('rstcheck-core'))" ``` validations: required: true rstcheck-core-1.2.1/.github/ISSUE_TEMPLATE/question.yaml000066400000000000000000000050441457751767200225550ustar00rootroot00000000000000--- 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 version are you using? You can find the rstcheck version with: Python >= 3.8: ```shell python -c "import importlib.metadata; print(importlib.metadata.version('rstcheck-core'))" ``` Python < 3.8: ```shell python -c "import importlib_metadata; print(importlib_metadata.version('rstcheck-core'))" ``` validations: required: true rstcheck-core-1.2.1/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000012711457751767200214360ustar00rootroot00000000000000 # 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. rstcheck-core-1.2.1/.github/dependabot.yml000066400000000000000000000001561457751767200204660ustar00rootroot00000000000000--- version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "daily" rstcheck-core-1.2.1/.github/workflows/000077500000000000000000000000001457751767200176715ustar00rootroot00000000000000rstcheck-core-1.2.1/.github/workflows/codeql-analysis.yml000066400000000000000000000045271457751767200235140ustar00rootroot00000000000000--- 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 rstcheck-core-1.2.1/.github/workflows/documentation.yml000066400000000000000000000022341457751767200232660ustar00rootroot00000000000000--- 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.11" - 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 rstcheck-core-1.2.1/.github/workflows/qa.yml000066400000000000000000000015031457751767200210140ustar00rootroot00000000000000--- 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.11" - 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 rstcheck-core-1.2.1/.github/workflows/test.yml000066400000000000000000000033451457751767200214000ustar00rootroot00000000000000--- 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-dev"] 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} rstcheck-core-1.2.1/.github/workflows/update-authors.yml000066400000000000000000000051741457751767200233700ustar00rootroot00000000000000--- 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.11" - 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 rstcheck-core-1.2.1/.gitignore000066400000000000000000000002731457751767200162660ustar00rootroot00000000000000.*.swp .eggs/ .tox/ dist build *.pyc *~ *.egg-info *.aux *.toc *.log *.nav *.out *.snm Pipfile.lock *.bak.* objects.*.txt .coverage_cache/ .ruff_cache/ .flakeheaven_cache/ __version__.py rstcheck-core-1.2.1/.markdownlint-cli2.jsonc000066400000000000000000000012441457751767200207510ustar00rootroot00000000000000// 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", }, }, } rstcheck-core-1.2.1/.pre-commit-config.yaml000066400000000000000000000162341457751767200205630ustar00rootroot00000000000000--- minimum_pre_commit_version: "2.17" default_stages: [commit] ci: skip: [mypy] 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: c4a0b883114b00d8d76b479c820ce7950211c99b # frozen: v4.5.0 hooks: - id: check-case-conflict - id: check-shebang-scripts-are-executable # #################################################################################### # # FORMATTING # # #################################################################################### # ---------------------------------------------- # Python # ---------------------------------------------- # ruff - python linter with fixing ability - repo: https://github.com/astral-sh/ruff-pre-commit rev: c22645f6b45188216151a407c040a9eec1795ab0 # frozen: v0.3.3 hooks: - id: ruff name: ruff (fix) args: ["--fix-only", "--exit-non-zero-on-fix", "--config=pyproject.toml"] # black - python formatter - repo: https://github.com/psf/black rev: 552baf822992936134cbd31a38f69c8cfe7c0f05 # frozen: 24.3.0 hooks: - id: black args: ["--safe"] # blacken-docs - black for python code in docs (rst/md/tex) - repo: https://github.com/asottile/blacken-docs rev: 960ead214cd1184149d366c6d27ca6c369ce46b6 # frozen: 1.16.0 hooks: - id: blacken-docs 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: c4a0b883114b00d8d76b479c820ce7950211c99b # frozen: v4.5.0 hooks: - id: end-of-file-fixer stages: [commit] - id: trailing-whitespace stages: [commit] # #################################################################################### # # LINTING # # #################################################################################### # ---------------------------------------------- # General (code unspecific) # ---------------------------------------------- - repo: local hooks: # Find TODO:|FIXME:|BUG: comments in all files # Inline skip: `#i#` directly after the colon after the tag-word - id: find-todos name: "Find TODO:|FIXME:|BUG: comments" description: "Check for TODO:|FIXME:|BUG: comments in all files" language: pygrep entry: '(^|//!?|#| ## Unreleased [diff v1.2.1...main](https://github.com/rstcheck/rstcheck-core/compare/v1.2.1...main) ## [1.2.1 (2024-03-23)](https://github.com/rstcheck/rstcheck-core/releases/v1.2.1) [diff vv1.2.0...v1.2.1](https://github.com/rstcheck/rstcheck-core/compare/vv1.2.0...v1.2.1) ### Miscellaneous - Fixed yaml tests running even when pyyaml is not installed ([#84](https://github.com/rstcheck/rstcheck-core/issues/84)) - Drop support for sphinx v4 ([#90](https://github.com/rstcheck/rstcheck-core/pull/90)) ## [1.2.0 (2023-11-13)](https://github.com/rstcheck/rstcheck-core/releases/v1.2.0) [diff v1.1.1...v1.2.0](https://github.com/rstcheck/rstcheck-core/compare/v1.1.1...v1.2.0) ### Miscellaneous - Remove unused pre python 3.8 compatibility code ([#74](https://github.com/rstcheck/rstcheck-core/pull/74)) - Add optional YAML code block support ([#77](https://github.com/rstcheck/rstcheck-core/issues/77)) - Improve log message content and reduce log level to warning for missing line numbers in literal blocks ([#81](https://github.com/rstcheck/rstcheck-core/issues/81)) ## [1.1.1 (2023-09-09)](https://github.com/rstcheck/rstcheck-core/releases/v1.1.1) [diff v1.1.0...v1.1.1](https://github.com/rstcheck/rstcheck-core/compare/v1.1.0...v1.1.1) ### Bugfixes - Use real filename instead of tempfile name for C/C++/rst code blocks ([#64](https://github.com/rstcheck/rstcheck-core/issues/64)) ### Miscellaneous - Reduce log level and make message more clear for the AttributeError issue ([#63](https://github.com/rstcheck/rstcheck-core/issues/63)) ## [1.1.0 (2023-09-09)](https://github.com/rstcheck/rstcheck-core/releases/v1.1.0) [diff v1.0.3...v1.1.0](https://github.com/rstcheck/rstcheck-core/compare/v1.0.3...v1.1.0) ### Bugfixes - Auto discover pyproject.toml file on py311 and up ### Documentation - Update inv file for pydantic links ([#60](https://github.com/rstcheck/rstcheck-core/pull/60)) ### Miscellaneous - Ignore "no newline at end of file" errors when C++ code is checked by clang (such as on macOS) ([#45](https://github.com/rstcheck/rstcheck-core/pull/45)) - Drop python 3.7 ([#52](https://github.com/rstcheck/rstcheck-core/pull/52)) - Drop support for Sphinx v2 and v3 ([#51](https://github.com/rstcheck/rstcheck-core/pull/51)) - Add tox environments for v6 and v7 ([#51](https://github.com/rstcheck/rstcheck-core/pull/51)) - Add basic pydantic v2 support ([#53](https://github.com/rstcheck/rstcheck-core/pull/53)) - Update Sphinx Theme Version and remove outdated Dark Mode Lib ([#51](https://github.com/rstcheck/rstcheck-core/pull/51)) - Switch from poetry to setuptools ([#59](https://github.com/rstcheck/rstcheck-core/pull/59)) - Change test file naming convention ([#60](https://github.com/rstcheck/rstcheck-core/pull/60)) - Change dev tooling ([#60](https://github.com/rstcheck/rstcheck-core/pull/60)) - Drop pydantic v1 support ([#60](https://github.com/rstcheck/rstcheck-core/pull/60)) - Add python 3.12 to CI ([#60](https://github.com/rstcheck/rstcheck-core/pull/60)) ## [1.0.3 (2022-11-12)](https://github.com/rstcheck/rstcheck-core/releases/v1.0.3) [diff v1.0.2...v1.0.3](https://github.com/rstcheck/rstcheck-core/compare/v1.0.2...v1.0.3) ### Documentation - Update release docs for changed release script - Restructure FAQ in docs ### Miscellaneous - Fix release script's changelog insertion - Add pre-commit-ci badge to README - Update development tooling dependencies - Update GHA workflows ([#22](https://github.com/rstcheck/rstcheck-core/issues/22)) - Add support for python 3.11 ([#21](https://github.com/rstcheck/rstcheck-core/issues/21)) - Update docutils version constraint ([#20](https://github.com/rstcheck/rstcheck-core/issues/20)) ## [1.0.2 (2022-05-30)](https://pypi.org/project/rstcheck-core/1.0.2) [diff v1.0.1.post2...v1.0.2](https://github.com/rstcheck/rstcheck-core/compare/v1.0.1.post2...v1.0.2) ### Miscellaneous - Add tox envs to test with sphinx v5. - Widen version range for `sphinx` extra to include v5. - Update `sphinx` `extlinks` config for v5. - Print error message on non-zero exit code ([#15](https://github.com/rstcheck/rstcheck-core/pull/15)) - Add integration tests based off of cli tests from `rstcheck` cli app ([#16](https://github.com/rstcheck/rstcheck-core/pull/16)) ## 1.0.1.post2 (2022-05-30) [diff v1.0.1.post1...v1.0.1.post2](https://github.com/rstcheck/rstcheck-core/compare/v1.0.1.post1...v1.0.1.post2) ### Miscellaneous - Fix link for PyPI in changelog for 1.0.1 release. - Fix link for PyPI in release script. ## 1.0.1.post1 (2022-05-30) [diff v1.0.1...v1.0.1.post1](https://github.com/rstcheck/rstcheck-core/compare/v1.0.1...v1.0.1.post1) ### Miscellaneous - Update changelog with missing notes ## [1.0.1 (2022-05-30)](https://pypi.org/project/rstcheck-core/1.0.1) [diff v1.0.0...v1.0.1](https://github.com/rstcheck/rstcheck-core/compare/v1.0.0...v1.0.1) ### New features - Add function to create dummy `Sphinx` app ([#13](https://github.com/rstcheck/rstcheck-core/pull/13)) ### Bugfixes - Fix `sourcecode` directive being ignored, when Sphinx support is active ([#13](https://github.com/rstcheck/rstcheck-core/pull/13)) ### Documentation - Add section to FAQ about issue with language-less code blocks with sphinx ([#13](https://github.com/rstcheck/rstcheck-core/pull/13)) ### Miscellaneous - Changed log level for unparsable GCC style messages from WARNING to DEBUG to reduce noise. - Log CRITICAL on AttributeError with sphinx support on, which mostly probably comes from language-less code blocks ([#13](https://github.com/rstcheck/rstcheck-core/pull/13)) ## v1.0.0 (2022-05-29) [diff v1.0.0rc1...v1.0.0](https://github.com/rstcheck/rstcheck-core/compare/v1.0.0rc1...v1.0.0) ### New features - Non-existing paths are filtered out before checking and are logged as warning ([#10](https://github.com/rstcheck/rstcheck-core/pull/10)) - Use `` for source in error messages instead of `-` ([#11](https://github.com/rstcheck/rstcheck-core/pull/11)) - Add constructor function for `IgnoreDict` ([#12](https://github.com/rstcheck/rstcheck-core/pull/12)) ## v1.0.0rc1 (2022-05-28) [diff split...v1.0.0](https://github.com/rstcheck/rstcheck-core/compare/split...v1.0.0rc1) - Initial version after code base split from [rstcheck/rstcheck](https://github.com/rstcheck/rstcheck) rstcheck-core-1.2.1/LICENSE000066400000000000000000000020451457751767200153020ustar00rootroot00000000000000Copyright (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 "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. rstcheck-core-1.2.1/MANIFEST.in000066400000000000000000000003161457751767200160320ustar00rootroot00000000000000graft src graft testing graft tests prune docs exclude * include AUTHORS.rst include CHANGELOG.md include LICENSE include MANIFEST.in include pyproject.toml include README.rst global-exclude *~ *.py[cod] rstcheck-core-1.2.1/README.rst000066400000000000000000000155221457751767200157700ustar00rootroot00000000000000============= rstcheck-core ============= +-------------------+---------------------------------------------------------------------------------------------+ | **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| | +-------------------+---------------------------------------------------------------------------------------------+ Library for checking syntax of reStructuredText and code blocks nested within it. See the full documentation at `read-the-docs`_ .. contents:: Installation ============ From pip .. code:: shell $ pip install rstcheck_core To use pyproject.toml for configuration:: $ pip install rstcheck_core[toml] To add sphinx support:: $ pip install rstcheck_core[sphinx] To add YAML checking support:: $ pip install rstcheck_core[yaml] Supported languages in code blocks ================================== - Bash - Doctest - C (C99) - C++ (C++11) - JSON - XML - Python - reStructuredText - YAML .. _read-the-docs: https://rstcheck-core.readthedocs.io .. General .. |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-core.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-core/latest.svg?style=flat-square&logo=read-the-docs&logoColor=white&label=Read%20the%20Docs :target: https://rstcheck-core.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-core/test.yml?branch=main&style=flat-square&logo=github&label=Test%20code :target: https://github.com/rstcheck/rstcheck-core/actions/workflows/test.yaml :alt: Test status .. |gha_docu| image:: https://img.shields.io/github/actions/workflow/status/rstcheck/rstcheck-core/documentation.yml?branch=main&style=flat-square&logo=github&label=Test%20documentation :target: https://github.com/rstcheck/rstcheck-core/actions/workflows/documentation.yaml :alt: Documentation status .. |gha_qa| image:: https://img.shields.io/github/actions/workflow/status/rstcheck/rstcheck-core/qa.yml?branch=main&style=flat-square&logo=github&label=QA :target: https://github.com/rstcheck/rstcheck-core/actions/workflows/qa.yaml :alt: QA status .. |pre_commit_ci| image:: https://results.pre-commit.ci/badge/github/rstcheck/rstcheck-core/main.svg :target: https://results.pre-commit.ci/latest/github/rstcheck-core/rstcheck/main :alt: pre-commit status .. PyPI .. |pypi_release| image:: https://img.shields.io/pypi/v/rstcheck-core.svg?style=flat-square&logo=pypi&logoColor=FBE072 :target: https://pypi.org/project/rstcheck-core/ :alt: PyPI - Package latest release .. |pypi_py_versions| image:: https://img.shields.io/pypi/pyversions/rstcheck-core.svg?style=flat-square&logo=python&logoColor=FBE072 :target: https://pypi.org/project/rstcheck-core/ :alt: PyPI - Supported Python Versions .. |pypi_implementations| image:: https://img.shields.io/pypi/implementation/rstcheck-core.svg?style=flat-square&logo=python&logoColor=FBE072 :target: https://pypi.org/project/rstcheck-core/ :alt: PyPI - Supported Implementations .. |pypi_format| image:: https://img.shields.io/pypi/format/rstcheck-core.svg?style=flat-square&logo=pypi&logoColor=FBE072 :target: https://pypi.org/project/rstcheck-core/ :alt: PyPI - Format .. |pypi_downloads| image:: https://img.shields.io/pypi/dm/rstcheck-core.svg?style=flat-square&logo=pypi&logoColor=FBE072 :target: https://pypi.org/project/rstcheck-core/ :alt: PyPI - Monthly downloads .. GitHub .. |gh_tag| image:: https://img.shields.io/github/v/tag/rstcheck/rstcheck-core.svg?sort=semver&style=flat-square&logo=github :target: https://github.com/rstcheck/rstcheck-core/tags :alt: Github - Latest Release .. |gh_last_commit| image:: https://img.shields.io/github/last-commit/rstcheck/rstcheck-core.svg?style=flat-square&logo=github :target: https://github.com/rstcheck/rstcheck-core/commits/main :alt: GitHub - Last Commit .. |gh_stars| image:: https://img.shields.io/github/stars/rstcheck/rstcheck-core.svg?style=flat-square&logo=github :target: https://github.com/rstcheck/rstcheck-core/stargazers :alt: Github - Stars .. |gh_forks| image:: https://img.shields.io/github/forks/rstcheck/rstcheck-core.svg?style=flat-square&logo=github :target: https://github.com/rstcheck/rstcheck-core/network/members :alt: Github - Forks .. |gh_contributors| image:: https://img.shields.io/github/contributors/rstcheck/rstcheck-core.svg?style=flat-square&logo=github :target: https://github.com/rstcheck/rstcheck-core/graphs/contributors :alt: Github - Contributors .. |gh_watchers| image:: https://img.shields.io/github/watchers/rstcheck/rstcheck-core.svg?style=flat-square&logo=github :target: https://github.com/rstcheck/rstcheck-core/watchers/ :alt: Github - Watchers rstcheck-core-1.2.1/docs/000077500000000000000000000000001457751767200152245ustar00rootroot00000000000000rstcheck-core-1.2.1/docs/source/000077500000000000000000000000001457751767200165245ustar00rootroot00000000000000rstcheck-core-1.2.1/docs/source/_apidoc_templates/000077500000000000000000000000001457751767200222005ustar00rootroot00000000000000rstcheck-core-1.2.1/docs/source/_apidoc_templates/module.rst_t000066400000000000000000000003031457751767200245360ustar00rootroot00000000000000{%- if show_headings %} {{- [basename, "module"] | join(' ') | e | heading }} {% endif -%} .. automodule:: {{ qualname }} {%- for option in automodule_options %} :{{ option }}: {%- endfor %} rstcheck-core-1.2.1/docs/source/_apidoc_templates/package.rst_t000066400000000000000000000021241457751767200246470ustar00rootroot00000000000000{%- macro automodule(modname, options) -%} .. automodule:: {{ modname }} {%- for option in options %} :{{ option }}: {%- endfor %} {%- endmacro %} {%- macro toctree(docnames) -%} .. toctree:: :maxdepth: {{ maxdepth }} {% for docname in docnames %} {{ docname }} {%- endfor %} {%- endmacro %} {%- if is_namespace %} {{- [pkgname, "namespace"] | join(" ") | e | heading }} {% else %} {{- [pkgname, "package"] | join(" ") | e | heading }} {% endif %} {%- if modulefirst and not is_namespace %} {{ automodule(pkgname, automodule_options) }} {% endif %} {%- if subpackages %} Subpackages ----------- {{ toctree(subpackages) }} {% endif %} {%- if submodules %} Submodules ---------- {% if separatemodules %} {{ toctree(submodules) }} {% else %} {%- for submodule in submodules %} {% if show_headings %} {{- [submodule, "module"] | join(" ") | e | heading(2) }} {% endif %} {{ automodule(submodule, automodule_options) }} {% endfor %} {%- endif %} {%- endif %} {%- if not modulefirst and not is_namespace %} Module contents --------------- {{ automodule(pkgname, automodule_options) }} {% endif %} rstcheck-core-1.2.1/docs/source/_apidoc_templates/toc.rst_t000066400000000000000000000001771457751767200240470ustar00rootroot00000000000000{{ header | heading }} .. toctree:: :maxdepth: {{ maxdepth }} {% for docname in docnames %} {{ docname }} {%- endfor %} rstcheck-core-1.2.1/docs/source/_badges.rst000066400000000000000000000137741457751767200206560ustar00rootroot00000000000000+-------------------+---------------------------------------------------------------------------------------------+ | **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-core.svg?style=flat-square&label=License :target: https://github.com/rstcheck/rstcheck-core/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-core.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-core/test.yml?branch=main&style=flat-square&logo=github&label=Test%20code :target: https://github.com/rstcheck/rstcheck-core/actions/workflows/test.yaml :alt: Test status .. |gha_docu| image:: https://img.shields.io/github/actions/workflow/status/rstcheck/rstcheck-core/documentation.yml?branch=main&style=flat-square&logo=github&label=Test%20documentation :target: https://github.com/rstcheck/rstcheck-core/actions/workflows/documentation.yaml :alt: Documentation status .. |gha_qa| image:: https://img.shields.io/github/actions/workflow/status/rstcheck/rstcheck-core/qa.yml?branch=main&style=flat-square&logo=github&label=QA :target: https://github.com/rstcheck/rstcheck-core/actions/workflows/qa.yaml :alt: QA status .. PyPI .. |pypi_release| image:: https://img.shields.io/pypi/v/rstcheck-core.svg?style=flat-square&logo=pypi&logoColor=FBE072 :target: https://pypi.org/project/rstcheck-core/ :alt: PyPI - Package latest release .. |pypi_py_versions| image:: https://img.shields.io/pypi/pyversions/rstcheck-core.svg?style=flat-square&logo=python&logoColor=FBE072 :target: https://pypi.org/project/rstcheck-core/ :alt: PyPI - Supported Python Versions .. |pypi_implementations| image:: https://img.shields.io/pypi/implementation/rstcheck-core.svg?style=flat-square&logo=python&logoColor=FBE072 :target: https://pypi.org/project/rstcheck-core/ :alt: PyPI - Supported Implementations .. |pypi_format| image:: https://img.shields.io/pypi/format/rstcheck-core.svg?style=flat-square&logo=pypi&logoColor=FBE072 :target: https://pypi.org/project/rstcheck-core/ :alt: PyPI - Format .. |pypi_downloads| image:: https://img.shields.io/pypi/dm/rstcheck-core.svg?style=flat-square&logo=pypi&logoColor=FBE072 :target: https://pypi.org/project/rstcheck-core/ :alt: PyPI - Monthly downloads .. GitHub .. |gh_tag| image:: https://img.shields.io/github/v/tag/rstcheck/rstcheck-core.svg?sort=semver&style=flat-square&logo=github :target: https://github.com/rstcheck/rstcheck-core/tags :alt: Github - Latest Release .. |gh_last_commit| image:: https://img.shields.io/github/last-commit/rstcheck/rstcheck-core.svg?style=flat-square&logo=github :target: https://github.com/rstcheck/rstcheck-core/commits/main :alt: GitHub - Last Commit .. |gh_stars| image:: https://img.shields.io/github/stars/rstcheck/rstcheck-core.svg?style=flat-square&logo=github :target: https://github.com/rstcheck/rstcheck-core/stargazers :alt: Github - Stars .. |gh_forks| image:: https://img.shields.io/github/forks/rstcheck/rstcheck-core.svg?style=flat-square&logo=github :target: https://github.com/rstcheck/rstcheck-core/network/members :alt: Github - Forks .. |gh_contributors| image:: https://img.shields.io/github/contributors/rstcheck/rstcheck-core.svg?style=flat-square&logo=github :target: https://github.com/rstcheck/rstcheck-core/graphs/contributors :alt: Github - Contributors .. |gh_watchers| image:: https://img.shields.io/github/watchers/rstcheck/rstcheck-core.svg?style=flat-square&logo=github :target: https://github.com/rstcheck/rstcheck-core/watchers/ :alt: Github - Watchers rstcheck-core-1.2.1/docs/source/api.rst000066400000000000000000000005061457751767200200300ustar00rootroot00000000000000=== API === This part of the documentation covers the open API of ``rstcheck-core`` and is auto generated by sphinx-apidoc via sphinxcontrib-apidoc. rst-source links (top right) do not work here, as auto generated api documentation files are discarded after build. .. toctree:: :maxdepth: 4 autoapidoc/rstcheck_core rstcheck-core-1.2.1/docs/source/authors.rst000066400000000000000000000000601457751767200207370ustar00rootroot00000000000000Authors ======= .. include:: ../../AUTHORS.rst rstcheck-core-1.2.1/docs/source/autoapidoc/000077500000000000000000000000001457751767200206545ustar00rootroot00000000000000rstcheck-core-1.2.1/docs/source/autoapidoc/rstcheck_core.rst000066400000000000000000000016441457751767200242310ustar00rootroot00000000000000rstcheck\_core package ====================== .. automodule:: rstcheck_core :members: :undoc-members: :show-inheritance: Submodules ---------- rstcheck\_core.checker module ----------------------------- .. automodule:: rstcheck_core.checker :members: :undoc-members: :show-inheritance: rstcheck\_core.config module ---------------------------- .. automodule:: rstcheck_core.config :members: :undoc-members: :show-inheritance: rstcheck\_core.inline\_config module ------------------------------------ .. automodule:: rstcheck_core.inline_config :members: :undoc-members: :show-inheritance: rstcheck\_core.runner module ---------------------------- .. automodule:: rstcheck_core.runner :members: :undoc-members: :show-inheritance: rstcheck\_core.types module --------------------------- .. automodule:: rstcheck_core.types :members: :undoc-members: :show-inheritance: rstcheck-core-1.2.1/docs/source/changelog.rst000066400000000000000000000000421457751767200212010ustar00rootroot00000000000000.. mdinclude:: ../../CHANGELOG.md rstcheck-core-1.2.1/docs/source/conf.py000066400000000000000000000152051457751767200200260ustar00rootroot00000000000000"""Configuration file for the Sphinx documentation builder.""" from __future__ import annotations import datetime import os import re import shutil 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: import sphinx.ext.autodoc 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_core" author = "Steven Myint " GH_REPO_LINK = "https://github.com/rstcheck/rstcheck-core" 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_CORE_VERSION = metadata(project)["Version"] release = RSTCHECK_CORE_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") #: -- M2R2 ----------------------------------------------------------------------------- extensions.append("m2r2") source_suffix = [".rst", ".md"] #: -- 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"), "pydantic": ("https://docs.pydantic.dev/latest/", "objects.pydantic.inv"), "sphinx": ("https://www.sphinx-doc.org/en/master/extdev/", "objects.sphinx.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"), } #: -- APIDOC --------------------------------------------------------------------------- apidoc_module_dir = f"../../src/{project}/" apidoc_output_dir = "autoapidoc" apidoc_toc_file = False apidoc_separate_modules = False apidoc_module_first = True apidoc_extra_args = [] if Path("_apidoc_templates").is_dir(): apidoc_extra_args += ["--templatedir", "apidoc_templates"] autoclass_content = "both" if find_spec("sphinxcontrib.apidoc") is not None: extensions.append("sphinxcontrib.apidoc") if Path(apidoc_output_dir).is_dir(): shutil.rmtree(apidoc_output_dir) else: NOT_LOADED_MSGS.append("## 'sphinxcontrib-apidoc' extension not loaded - not installed") #: -- AUTODOC -------------------------------------------------------------------------- extensions.append("sphinx.ext.autodoc") autodoc_typehints = "description" autodoc_member_order = "bysource" autodoc_mock_imports: list[str] = [] autodoc_default_options = {"members": True} def _remove_module_docstring( app: Sphinx, # noqa: ARG001 what: str, name: str, # noqa: ARG001 obj: t.Any, # noqa: ANN401,ARG001 options: sphinx.ext.autodoc.Options, # noqa: ARG001 lines: list[str], ) -> None: """Remove module docstring.""" if what == "module": del lines[:] if find_spec("sphinx_autodoc_typehints") is not None: extensions.append("sphinx_autodoc_typehints") else: NOT_LOADED_MSGS.append("## 'sphinx-autodoc-typehints' extension not loaded - not installed") #: -- SPELLING ------------------------------------------------------------------------- spelling_word_list_filename = "spelling_dict.txt" spelling_show_suggestions = True spelling_exclude_patterns = ["autoapi/**", "autoapidoc/**"] 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.connect("autodoc-process-docstring", _remove_module_docstring) app.add_config_value("RELEASE_LEVEL", "", "env") for msg in NOT_LOADED_MSGS: print(msg) # noqa: T201 rstcheck-core-1.2.1/docs/source/faq.rst000066400000000000000000000031531457751767200200270ustar00rootroot00000000000000FAQ / Known limitations / Known issues ====================================== Known limitations ----------------- There are inherent limitations to what ``rstcheck-core`` can and cannot do. The reason for this is that ``rstcheck-core`` 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-core`` is more like an error accumulation tool. The same goes for the source code in supported code blocks. Known issues ------------ Code blocks without language (Sphinx) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ According to the `documentation for reStructuredText`_ over at docutils the language is an optional argument to a code block directive. In vanilla mode language-less code blocks are treated like code blocks which specified a language that is not supported by ``rstcheck``. When sphinx support is active however, a deeply nested issue arises in form of an :py:exc:`AttributeError`. This exception has the unpleasant side-effect that linting for the whole file containing this language-less code block fails. This may result in a false negative for the file. **There are currently only one fix available:** Explicitly specifying the language for the code block. This renders the ``highlight`` directive useless, but is the only known way to fix this issue. And it enables checks of the source code inside those code blocks, if the language is supported of cause. This issue is tracked in issue :issue:`3`. .. _documentation for reStructuredText: https://docutils.sourceforge.io/docs/ref/rst/directives.html#code rstcheck-core-1.2.1/docs/source/index.rst000066400000000000000000000011071457751767200203640ustar00rootroot00000000000000Welcome to rstcheck-core's documentation! ========================================= .. include:: _badges.rst This is the documentation of ``rstcheck-core``, the core library behind the CLI application ``rstcheck`` 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: API Reference: api .. toctree:: :maxdepth: 2 :caption: Miscellaneous: faq migrations changelog authors license rstcheck-core-1.2.1/docs/source/installation.rst000066400000000000000000000027361457751767200217670ustar00rootroot00000000000000.. 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-core Extras ~~~~~~ ``rstcheck-core`` 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-core[sphinx,toml] Installation from source ------------------------ You can install ``rstcheck-core`` 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-core $ cd rstcheck-core $ pip install . Or installing directly via git:: $ pip install git+https://github.com/rstcheck/rstcheck-core 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/ rstcheck-core-1.2.1/docs/source/license.rst000066400000000000000000000000631457751767200206770ustar00rootroot00000000000000License ======= .. literalinclude:: ../../LICENSE rstcheck-core-1.2.1/docs/source/migrations.rst000066400000000000000000000011661457751767200214360ustar00rootroot00000000000000.. 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:: rstcheck v5 to rstcheck-core v1 (rc1) ------------------------------------- **Subject to change till final release** With version 6 the whole code base was restructured and the core library was moved into a separate repository. With it the whole open library API has changed. A simple migration is not possible. Please see the :ref:`api:API` section or open an issue if you have open questions. .. highlight:: default rstcheck-core-1.2.1/docs/source/objects.pattern.inv000066400000000000000000000002431457751767200223460ustar00rootroot00000000000000# Sphinx inventory version 2 # Project: rstcheck # Version: 6.0 # The remainder of this file is compressed using zlib. x H,)I-S(JI,.V0T(,K(QUtLrstcheck-core-1.2.1/docs/source/objects.pydantic.inv000066400000000000000000000003361457751767200225070ustar00rootroot00000000000000# Sphinx inventory version 2 # Project: rstcheck # Version: 6.0 # The remainder of this file is compressed using zlib. xm; 0{ON`ikn,$ِM>Jh"P2c)>W^Ķ ~HoW_g@*qK_zgE/rstcheck-core-1.2.1/docs/source/objects.sphinx.inv000066400000000000000000000002571457751767200222070ustar00rootroot00000000000000# Sphinx inventory version 2 # Project: rstcheck # Version: 6.0 # The remainder of this file is compressed using zlib. x+.̫K,(LN,  )TZ%$+*e 22JrsUt$rstcheck-core-1.2.1/docs/source/spelling_dict.txt000066400000000000000000000013221457751767200221030ustar00rootroot00000000000000api apidoc arg args attr autoapidoc bool boolean bugfixes cfg changelog ci cielquan cli cmd compat conf config configs csv deleter dependabot dev dir dirs docstring docstrings doctest dotenv env envs envvar exe executable executables expr fmt formatter formatters func gcc getter github html importlib ini init inv kwarg kwargs linter linters macOS makefile monkypatch monkypatched multiline nd noqa nosec nox nox's noxfile num opensource os parametrize pragma pre py pyproject pytest pyyaml rc rd reStructuredText redef repo riedel rst rstcheck rtd sdist setter sphinxcontrib st stdin submodule submodules temp testcleanup testsetup tmp toctree tokenize toml tox travis txt unparsable venv virtualenv whitespace xml yaml rstcheck-core-1.2.1/docs/source/usage/000077500000000000000000000000001457751767200176305ustar00rootroot00000000000000rstcheck-core-1.2.1/docs/source/usage/config.rst000066400000000000000000000232521457751767200216330ustar00rootroot00000000000000Configuration ============= .. contents:: ``rstcheck-core``'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 Configuration files ~~~~~~~~~~~~~~~~~~~ ``rstcheck-core`` 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-core`` 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-core`` 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 if the used python version is lower than 3.11. See the :ref:`installation:Installation` section for more information. In TOML format all config related to ``rstcheck-core`` must go into the ``[tool.rstcheck]`` dictionary. This is due to the python convention for the ``pyproject.toml`` file, which ``rstcheck-core`` 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``) - 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,...``) - 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,...``) - 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,...``) - 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,...``) - 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 - YAML Ignore specific error messages ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Supported sources: - CLI (``--ignore-messages REGEX_STRING``) - 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: 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-core``. 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. rstcheck-core-1.2.1/docs/source/usage/index.rst000066400000000000000000000002671457751767200214760ustar00rootroot00000000000000Usage ===== ``rstcheck-core`` is a library and the heart of ``rstcheck``. Here you can find how to use it. .. toctree:: :maxdepth: 1 :caption: Contents: config library rstcheck-core-1.2.1/docs/source/usage/library.rst000066400000000000000000000023741457751767200220340ustar00rootroot00000000000000Library ======= Documentation for the open API of the library can be found in the :ref:`api:API` section. Entry Points ------------ This library has two main entry points you can use: :py:class:`rstcheck_core.runner.RstcheckMainRunner` class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``RstcheckMainRunner`` class the is main entry point. It manages the configuration state, runs the check on the files, caches the found linting issues and prints them. :py:func:`rstcheck_core.checker.check_file` function ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``check_file`` function is a step deeper. It checks a single file and returns a list of the found linting issues. This would be the entry point if you don't need the additional management capabilities of the ``RstcheckMainRunner`` class. Logging ------- ``rstcheck-core`` uses the standard library's ``logging`` module for its logging functionality. Each python module has its own logger named after the ``__file__`` variable's value. Following the `Official HOWTO`_ logging is deactivated by default, but can be activated, if you as a developer provide a logging configuration. .. _Official HOWTO: https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library rstcheck-core-1.2.1/docs/source/workflows/000077500000000000000000000000001457751767200205615ustar00rootroot00000000000000rstcheck-core-1.2.1/docs/source/workflows/development.rst000066400000000000000000000042001457751767200236310ustar00rootroot00000000000000.. highlight:: console Development =========== ``rstcheck-core`` uses `Semantic Versioning`_. ``rstcheck-core`` 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-core`` 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``. .. highlight:: default .. _Semantic Versioning: https://semver.org/ rstcheck-core-1.2.1/docs/source/workflows/index.rst000066400000000000000000000002611457751767200224210ustar00rootroot00000000000000Workflows ========= Here is the documentation about different workflows for ``rstcheck-core``. .. toctree:: :maxdepth: 1 :caption: Contents: development releases rstcheck-core-1.2.1/docs/source/workflows/releases.rst000066400000000000000000000023021457751767200231130ustar00rootroot00000000000000.. 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 rstcheck-core-1.2.1/prep_release.py000066400000000000000000000125521457751767200173210ustar00rootroot00000000000000"""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-core" #: -- 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 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 'v'}{last_version}...v{new_version}" changelog_lines[release_line] = ( "## Unreleased\n" "\n" f"[diff v{new_version}...main]" f"({REPO_URL}/compare/v{new_version}...main)\n" "\n" f"## [{new_version} ({today})]({REPO_URL}/releases/v{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,S607 "git", "commit", "--no-verify", f'--message="release v{version} [skip ci]"', "--include", "pyproject.toml", "CHANGELOG.md", ], check=True, ) subprocess.run( ["git", "tag", "-am", f"'v{version}'", f"v{version}"], check=True # noqa: S603,S607 ) 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( ["git", "rev-list", "--max-parents=0", "HEAD"], # noqa: S603,S607 check=True, capture_output=True, ).stdout.decode()[0:7] else: git_tags = ( subprocess.run( ["git", "tag", "--list"], # noqa: S603,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()) rstcheck-core-1.2.1/pyproject.toml000066400000000000000000000164151457751767200172170ustar00rootroot00000000000000[build-system] requires = ["setuptools>=61", "wheel", "setuptools_scm[toml]>=6.2"] build-backend = "setuptools.build_meta" [project] name = "rstcheck-core" 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 = [ "docutils >=0.7", "pydantic >=2", "importlib-metadata >=1.6; python_version<='3.8'", "typing-extensions >=3.7.4; python_version<='3.8'", ] [project.optional-dependencies] sphinx = ["sphinx >=5.0"] toml = ["tomli >=2.0; python_version<='3.10'"] yaml = ["pyyaml >= 6.0.0"] 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", "pytest-mock >=3.7", ] docs = [ "sphinx >=5.0, !=7.2.5", "m2r2 >=0.3.2", "sphinx-rtd-theme >=1.2", "sphinx-autodoc-typehints >=1.15", "sphinxcontrib-apidoc >=0.3", "sphinxcontrib-spelling >=7.3", "sphinx-autobuild >=2021.3.14", ] type-check = [ "mypy >=1.0", "types-docutils >=0.18", "types-PyYAML >=6.0.0", ] dev = [ "rstcheck-core[sphinx,toml,testing,docs,type-check,yaml]", "tox >=3.15", ] [project.urls] Documentation = "https://rstcheck-core.readthedocs.io/en/latest/" Repository = "https://github.com/rstcheck/rstcheck-core" Changelog = "https://github.com/rstcheck/rstcheck-core/blob/main/CHANGELOG.md" [tool.setuptools_scm] write_to = "src/rstcheck_core/__version__.py" # -- BLACK CONFIG --------------------------------------------------------------------- [tool.black] line-length = 100 exclude = "venv/" include = '\.pyi?$' # single quotes needed # -- MYPY CONFIG ---------------------------------------------------------------------- [tool.mypy] show_error_codes = true strict_optional = true warn_unused_ignores = true warn_redundant_casts = true warn_return_any = true warn_unreachable = true disallow_any_generics = true check_untyped_defs = true implicit_reexport = false python_version = "3.11" # CHANGE ME plugins = "pydantic.mypy" [tool.pydantic-mypy] init_forbid_extra = true init_typed = false warn_required_dynamic_aliases = true warn_untyped_fields = true # -- RUFF CONFIG ---------------------------------------------------------------------- [tool.ruff] target-version = "py311" line-length = 100 output-format = "grouped" show-fixes = true show-source = true task-tags = ["TODO", "FIXME", "XXX", "NOTE", "BUG", "HACK", "CHANGE ME"] extend-exclude = [ "*venv*/", "*.bak.*", ] src = ["src", "tests"] unfixable = ["ERA001"] 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 "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 "G", # flake8-logging-format "I", # isort "ICN", # flake8-import-conventions "INP", # flake8-no-pep420 "INT", # flake8-gettext "ISC", # flake8-implicit-str-concat "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 "RUF", # Ruff-specific rules "RSE", # flake8-raise "S", # flake8-bandit "SLF", # flake8-self "SLOT", # flake8-slots "SIM", # flake8-simplify # "T10", # flake8-debugger # pre-commit hook does that "T20", # flake8-print "TCH", # flake8-type-checking # "TD", # flake8-todo "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 "PT011", # Use match for specific exceptions in pytest.raises ] [tool.ruff.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.flake8-annotations] suppress-dummy-args = true [tool.ruff.flake8-builtins] builtins-ignorelist = [ "id", ] [tool.ruff.flake8-import-conventions.extend-aliases] "typing" = "t" [tool.ruff.flake8-pytest-style] fixture-parentheses = false mark-parentheses = false [tool.ruff.flake8-type-checking] runtime-evaluated-base-classes = [ "pydantic.BaseModel", ] [tool.ruff.isort] combine-as-imports = true required-imports = [ "from __future__ import annotations", ] [tool.ruff.mccabe] max-complexity = 20 [tool.ruff.pep8-naming] classmethod-decorators = [ "pydantic.field_validator", ] staticmethod-decorators = [] [tool.ruff.pycodestyle] ignore-overlong-task-comments = true max-doc-length = 100 [tool.ruff.pydocstyle] convention = "pep257" ignore-decorators = [ "typing.overload", ] property-decorators = [] [tool.ruff.pylint] max-args = 6 # -- PYTEST CONFIG -------------------------------------------------------------------- [tool.pytest.ini_options] addopts = "-ra --showlocals" # -- 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_core", "*/.tox/*/lib/python*/site-packages/rstcheck_core", "*/.tox/pypy*/site-packages/rstcheck_core", "*/.tox\\*\\Lib\\site-packages\\rstcheck_core", ] local_combine = [ "src/rstcheck_core", "*/.venv/lib/python*/site-packages/rstcheck_core", "*/.venv\\*\\Lib\\site-packages\\rstcheck_core", "*/src/rstcheck_core", "*\\src\\rstcheck_core", ] [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 MYPY:", ] [tool.coverage.html] show_contexts = true [tool.coverage.coverage_conditional_plugin.rules] # use with: # pragma: py-gte-310 = "sys_version_info >= (3, 10)" rstcheck-core-1.2.1/src/000077500000000000000000000000001457751767200150635ustar00rootroot00000000000000rstcheck-core-1.2.1/src/rstcheck_core/000077500000000000000000000000001457751767200177015ustar00rootroot00000000000000rstcheck-core-1.2.1/src/rstcheck_core/__init__.py000066400000000000000000000002151457751767200220100ustar00rootroot00000000000000"""Rstcheck package.""" from __future__ import annotations import logging logging.getLogger("rstcheck").addHandler(logging.NullHandler()) rstcheck-core-1.2.1/src/rstcheck_core/_docutils.py000066400000000000000000000076341457751767200222520ustar00rootroot00000000000000"""Docutils helper functions.""" from __future__ import annotations import importlib import logging import typing as t import docutils.nodes import docutils.parsers.rst.directives import docutils.parsers.rst.roles import docutils.writers from . import _extras logger = logging.getLogger(__name__) class IgnoredDirective(docutils.parsers.rst.Directive): # pragma: no cover """Stub for unknown directives.""" has_content = True def run(self) -> list: # type: ignore[type-arg] """Do nothing.""" return [] def ignore_role( # noqa: PLR0913 name: str, # noqa: ARG001 rawtext: str, # noqa: ARG001 text: str, # noqa: ARG001 lineno: int, # noqa: ARG001 inliner: docutils.parsers.rst.states.Inliner, # noqa: ARG001 options: t.Mapping[str, t.Any] | None = None, # noqa: ARG001 content: t.Sequence[str] | None = None, # noqa: ARG001 ) -> tuple[ t.Sequence[docutils.nodes.reference], t.Sequence[docutils.nodes.reference] ]: # pragma: no cover """Stub for unknown roles.""" return ([], []) def clean_docutils_directives_and_roles_cache() -> None: # pragma: no cover """Clean docutils' directives and roles cache by reloading their modules. Reloads: - :py:mod:`docutils.parsers.rst.directives` - :py:mod:`docutils.parsers.rst.roles` """ logger.info("Reload module docutils.parsers.rst.directives/roles") importlib.reload(docutils.parsers.rst.directives) importlib.reload(docutils.parsers.rst.roles) def ignore_directives_and_roles(directives: list[str], roles: list[str]) -> None: """Ignore directives and roles in docutils. :param directives: Directives to ignore :param roles: Roles to ignore """ 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) class CodeBlockDirective(docutils.parsers.rst.Directive): """Code block directive.""" has_content = True optional_arguments = 1 def run(self) -> list[docutils.nodes.literal_block]: """Run directive. :return: Literal block """ 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( *, ignore_code_directive: bool = False, ignore_codeblock_directive: bool = False, ignore_sourcecode_directive: bool = False, ) -> None: """Optionally register code directives. :param ignore_code_directive: If "code" directive should be ignored, so that the code block will not be checked; defaults to :py:obj:`False` :param ignore_codeblock_directive: If "code-block" directive should be ignored, so that the code block will not be checked; defaults to :py:obj:`False` :param ignore_sourcecode_directive: If "sourcecode" directive should be ignored, so that the code block will not be checked; defaults to :py:obj:`False` """ if not _extras.SPHINX_INSTALLED: if ignore_code_directive is False: logger.debug("Register custom directive for 'code'.") docutils.parsers.rst.directives.register_directive("code", CodeBlockDirective) # NOTE: docutils maps `code-block` and `sourcecode` to `code` if ignore_codeblock_directive is False: logger.debug("Register custom directive for 'code-block'.") docutils.parsers.rst.directives.register_directive("code-block", CodeBlockDirective) if ignore_sourcecode_directive is False: logger.debug("Register custom directive for 'sourcecode'.") docutils.parsers.rst.directives.register_directive("sourcecode", CodeBlockDirective) rstcheck-core-1.2.1/src/rstcheck_core/_extras.py000066400000000000000000000066461457751767200217340ustar00rootroot00000000000000"""Central place for install-checker and guards for 'extras' dependencies. The ``*_INSTALLED`` constansts reveal wether the dependency is installed with a supported version. The :py:func:`install_guard` guard function is intended for use inside functions which need specific extra packages installed. Example usage: .. code-block:: python from rstcheck_core import _extras if _extras.SPHINX_INSTALLED: import sphinx def print_sphinx_version(): _extras.install_guard("sphinx") print(sphinx.version_info) """ from __future__ import annotations import importlib import importlib.metadata import logging import typing as t logger = logging.getLogger(__name__) ExtraDependencies = t.Literal["sphinx", "tomli"] """List of all dependencies installable through extras.""" class DependencyInfos(t.TypedDict): """Information about a dependency.""" min_version: tuple[int, ...] extra: str ExtraDependenciesInfos: dict[ExtraDependencies, DependencyInfos] = { "sphinx": DependencyInfos(min_version=(5, 0), extra="sphinx"), "tomli": DependencyInfos(min_version=(2, 0), extra="toml"), } """Dependency map with their min. supported version and extra by which they can be installed.""" def is_installed_with_supported_version(package: ExtraDependencies) -> bool: """Check if the package is installed and has the minimum required version. :param package: Name of packge to check :return: Bool if package is installed with supported version """ logger.debug( "Check if package is installed with supported version: '%s'.", package, ) try: importlib.import_module(package) except ImportError: return False version: str = importlib.metadata.version(package) version_tuple = tuple(int(v) for v in version.split(".")[:3]) return version_tuple >= ExtraDependenciesInfos[package]["min_version"] SPHINX_INSTALLED = is_installed_with_supported_version("sphinx") TOMLI_INSTALLED = is_installed_with_supported_version("tomli") ExtraDependenciesInstalled: dict[ExtraDependencies, bool] = { "sphinx": SPHINX_INSTALLED, "tomli": TOMLI_INSTALLED, } def install_guard(package: ExtraDependencies) -> None: """Guard code that needs the ``package`` installed and throw :py:exc:`ModuleNotFoundError`. See example in module docstring. :param package: Name of packge to check :raises ModuleNotFoundError: When the package is not installed. """ if ExtraDependenciesInstalled[package] is True: return extra = ExtraDependenciesInfos[package] msg = ( f"No supported version of {package} installed. Install rstcheck with " f"{extra} extra (rstcheck[{extra}]) or install a supported version of {package} yourself." ) raise ModuleNotFoundError(msg) def install_guard_tomli(*, tomllib_imported: bool) -> None: """Specific version of :py:func:`install_guard` for ``tomli``. :param tomllib_imported: If tomllib is imported :raises ModuleNotFoundError: When ``tomli`` is not installed. """ if tomllib_imported or ExtraDependenciesInstalled["tomli"] is True: return extra = ExtraDependenciesInfos["tomli"] msg = ( f"tomllib could not be imported and no supported version of tomli installed. " f"Install rstcheck with {extra} extra (rstcheck[{extra}]) or install a " "supported version of tomli yourself." ) raise ModuleNotFoundError(msg) rstcheck-core-1.2.1/src/rstcheck_core/_sphinx.py000066400000000000000000000073441457751767200217330ustar00rootroot00000000000000"""Sphinx helper functions.""" from __future__ import annotations import contextlib import logging import pathlib import tempfile import typing as t from . import _docutils, _extras if _extras.SPHINX_INSTALLED: import sphinx.application import sphinx.domains.c import sphinx.domains.cpp import sphinx.domains.javascript import sphinx.domains.python import sphinx.domains.std import sphinx.util.docutils logger = logging.getLogger(__name__) def create_dummy_sphinx_app() -> sphinx.application.Sphinx: """Create a dummy sphinx instance with temp dirs.""" logger.debug("Create dummy sphinx application.") with tempfile.TemporaryDirectory() as temp_dir: outdir = pathlib.Path(temp_dir) / "_build" return sphinx.application.Sphinx( srcdir=temp_dir, confdir=None, outdir=str(outdir), doctreedir=str(outdir), buildername="dummy", # NOTE: https://github.com/sphinx-doc/sphinx/issues/10483 status=None, ) @contextlib.contextmanager def load_sphinx_if_available() -> t.Generator[sphinx.application.Sphinx | None, None, None]: """Contextmanager to register Sphinx directives and roles if sphinx is available.""" if _extras.SPHINX_INSTALLED: create_dummy_sphinx_app() # NOTE: Hack to prevent sphinx warnings for overwriting registered nodes; see #113 sphinx.application.builtin_extensions = [ e for e in sphinx.application.builtin_extensions if e != "sphinx.addnodes" # type: ignore[assignment] ] yield None def get_sphinx_directives_and_roles() -> tuple[list[str], list[str]]: """Return Sphinx directives and roles loaded from sphinx. :return: Tuple of directives and roles """ _extras.install_guard("sphinx") 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, ]: domain_directives = list(domain.directives) domain_roles = list(domain.roles) sphinx_directives += domain_directives + [ f"{domain.name}:{item}" for item in domain_directives ] sphinx_roles += domain_roles + [f"{domain.name}:{item}" for item in domain_roles] sphinx_directives += list( sphinx.util.docutils.directives._directives # type: ignore[attr-defined] # noqa: SLF001 ) sphinx_roles += list( sphinx.util.docutils.roles._roles # type: ignore[attr-defined] # noqa: SLF001 ) return (sphinx_directives, sphinx_roles) _DIRECTIVE_WHITELIST = ["code", "code-block", "sourcecode", "include"] _ROLE_WHITELIST: list[str] = [] def filter_whitelisted_directives_and_roles( directives: list[str], roles: list[str] ) -> tuple[list[str], list[str]]: """Filter whitelisted directives and roles out of input. :param directives: Directives to filter :param roles: Roles to filter :return: Tuple of fitlered directives and roles """ directives = list(filter(lambda d: d not in _DIRECTIVE_WHITELIST, directives)) roles = list(filter(lambda r: r not in _ROLE_WHITELIST, roles)) return (directives, roles) def load_sphinx_ignores() -> None: # pragma: no cover """Register Sphinx directives and roles to ignore.""" _extras.install_guard("sphinx") logger.debug("Load sphinx directives and roles.") (directives, roles) = get_sphinx_directives_and_roles() (directives, roles) = filter_whitelisted_directives_and_roles(directives, roles) _docutils.ignore_directives_and_roles(directives, roles) rstcheck-core-1.2.1/src/rstcheck_core/checker.py000066400000000000000000001024701457751767200216630ustar00rootroot00000000000000"""Checking functionality.""" from __future__ import annotations import contextlib import copy import doctest import io import json import locale import logging import os import pathlib import re import shlex import subprocess import sys import tempfile import typing as t import warnings import xml.etree.ElementTree import docutils.core import docutils.io import docutils.nodes import docutils.utils from . import _docutils, _extras, _sphinx, config, inline_config, types try: import yaml yaml_imported = True except ImportError: yaml_imported = False logger = logging.getLogger(__name__) EXCEPTION_LINE_NO_REGEX = re.compile(r": line\s+([0-9]+)[^:]*$") DOCTEST_LINE_NO_REGEX = re.compile(r"line ([0-9]+)") MARKDOWN_LINK_REGEX = re.compile(r"\[[^\]]+\]\([^\)]+\)") def check_file( source_file: pathlib.Path, rstcheck_config: config.RstcheckConfig, overwrite_with_file_config: bool = True, # noqa: FBT001,FBT002 ) -> list[types.LintError]: """Check the given file for issues. On every call docutils' caches for roles and directives are cleared by reloading their modules. :param source_file: Path to file to check :param rstcheck_config: Main configuration of the application :param overwrite_with_file_config: If the loaded file config should overwrite the ``rstcheck_config``; defaults to :py:obj:`True` :return: A list of found issues """ logger.info("Check file'%s'", source_file) run_config = _load_run_config( source_file.parent, rstcheck_config, overwrite_config=overwrite_with_file_config ) ignore_dict = _create_ignore_dict_from_config(run_config) source = _get_source(source_file) _docutils.clean_docutils_directives_and_roles_cache() with _sphinx.load_sphinx_if_available(): return list( check_source( source, source_file=source_file, ignores=ignore_dict, report_level=run_config.report_level or config.DEFAULT_REPORT_LEVEL, warn_unknown_settings=run_config.warn_unknown_settings or False, ) ) def _load_run_config( source_file_dir: pathlib.Path, rstcheck_config: config.RstcheckConfig, *, overwrite_config: bool = True, ) -> config.RstcheckConfig: """Load file specific config file and create run config. If the ``rstcheck_config`` does not contain a ``config_path`` the ``source_file_dir`` directory tree is searched for a config file to load and merge into the ``rstcheck_config``. The merge strategy is set via ``overwrite_config``. :param source_file_dir: Directory of the current file to check :param rstcheck_config: Main configuration of the application :param overwrite_config: If the loaded config should overwrite the ``rstcheck_config``; defaults to :py:obj:`True` :return: Merged config """ if rstcheck_config.config_path is not None: return rstcheck_config file_config = config.load_config_file_from_dir_tree(source_file_dir) if file_config is None: return rstcheck_config return config.merge_configs( copy.copy(rstcheck_config), file_config, config_add_is_dominant=overwrite_config ) def _get_source(source_file: pathlib.Path) -> str: """Get source from file or stdin. If the file name is "-" then stdin is read for input instead of a file. :param source_file: File path to read contents from :return: Loaded content """ if source_file.name == "-": logger.info("Load source from stdin.") return sys.stdin.read() resolved_file_path = source_file.resolve() with contextlib.closing(docutils.io.FileInput(source_path=resolved_file_path)) as input_file: return input_file.read() def _replace_ignored_substitutions(source: str, ignore_substitutions: list[str]) -> str: """Replace rst substitutions from the ignore list with a dummy. :param source: Source to replace substitutions in :param ignore_substitutions: Substitutions to replace with dummy :return: Cleaned source """ for substitution in ignore_substitutions: source = source.replace(f"|{substitution}|", f"x{substitution}x") return source def _create_ignore_dict_from_config(rstcheck_config: config.RstcheckConfig) -> types.IgnoreDict: """Extract ignore settings from config and create a :py:class:`rstcheck_core.types.IgnoreDict`. :param rstcheck_config: Config to extract ignore settings from :return: :py:class:`rstcheck_core.types.IgnoreDict` """ return types.construct_ignore_dict( messages=rstcheck_config.ignore_messages, languages=rstcheck_config.ignore_languages, directives=rstcheck_config.ignore_directives, roles=rstcheck_config.ignore_roles, substitutions=rstcheck_config.ignore_substitutions, ) def check_source( source: str, source_file: types.SourceFileOrString | None = None, ignores: types.IgnoreDict | None = None, report_level: config.ReportLevel = config.DEFAULT_REPORT_LEVEL, *, warn_unknown_settings: bool = False, ) -> types.YieldedLintError: """Check the given rst source for issues. :param source_file: Path to file the source comes from if it comes from a file; defaults to :py:obj:`None` :param ignores: Ignore information; defaults to :py:obj:`None` :param report_level: Report level; defaults to :py:data:`rstcheck_core.config.DEFAULT_REPORT_LEVEL` :param warn_unknown_settings: If a warning should be logged for unknown settings in config file; defaults to :py:obj:`False` :return: :py:obj:`None` :yield: Found issues """ source_origin: types.SourceFileOrString = source_file or "" if isinstance(source_origin, pathlib.Path) and source_origin.name == "-": source_origin = "" logger.info("Check source from '%s'", source_origin) ignores = ignores or types.construct_ignore_dict() ignores["directives"].extend( inline_config.find_ignored_directives( source, source_origin, warn_unknown_settings=warn_unknown_settings ) ) ignores["roles"].extend( inline_config.find_ignored_roles( source, source_origin, warn_unknown_settings=warn_unknown_settings ) ) ignores["substitutions"].extend( inline_config.find_ignored_substitutions( source, source_origin, warn_unknown_settings=warn_unknown_settings ) ) ignores["languages"].extend( inline_config.find_ignored_languages( source, source_origin, warn_unknown_settings=warn_unknown_settings ) ) source = _replace_ignored_substitutions(source, ignores["substitutions"]) _docutils.register_code_directive( ignore_code_directive="code" in ignores["directives"], ignore_codeblock_directive="code-block" in ignores["directives"], ignore_sourcecode_directive="sourcecode" in ignores["directives"], ) _docutils.ignore_directives_and_roles(ignores["directives"] or [], ignores["roles"] or []) if _extras.SPHINX_INSTALLED: _sphinx.load_sphinx_ignores() writer = _CheckWriter(source, source_origin, ignores, report_level) 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. # This is tested in the CLI integration tests with the `testing/examples/good/bom.rst` file. with contextlib.suppress(UnicodeError): source = source.encode("utf-8").decode("utf-8-sig") with contextlib.suppress(docutils.utils.SystemMessage): # Sphinx will sometimes throw an `AttributeError` trying to access # "self.state.document.settings.env". Ignore this for now until we # figure out a better approach. # https://github.com/rstcheck/rstcheck-core/issues/3 try: docutils.core.publish_string( source, writer=writer, source_path=str(source_origin), settings_overrides={ "halt_level": 5, "report_level": report_level.value, "warning_stream": string_io, }, ) except AttributeError: if not _extras.SPHINX_INSTALLED: raise logger.warning( "An `AttributeError` error occured. This is most probably due to a code block " "directive (code/code-block/sourcecode) without a specified language. " "This may result in a false negative for source: '%s'. " "The reason can also be another directive. " "For more information see the FAQ (https://rstcheck-core.rtfd.io/en/latest/faq) " "or the corresponding github issue: " "https://github.com/rstcheck/rstcheck-core/issues/3.", source_origin, ) yield from _run_code_checker_and_filter_errors(writer.checkers, ignores["messages"]) rst_errors = string_io.getvalue().strip() if not rst_errors: return yield from _parse_and_filter_rst_errors(rst_errors, source_origin, ignores["messages"]) def _run_code_checker_and_filter_errors( checker_list: list[types.CheckerRunFunction], ignore_messages: t.Pattern[str] | None = None, ) -> types.YieldedLintError: """Run all code block checker functions. :param checker_list: List of code block checker functions :param ignore_messages: Regex for ignoring error messages; defaults to :py:obj:`None` :return: :py:obj:`None` :yield: Filtered :py:class:`rstcheck_core.types.LintError` s from run checker function """ for checker in checker_list: for lint_error in checker(): if ignore_messages and ignore_messages.search(lint_error["message"]): continue yield lint_error def _parse_and_filter_rst_errors( rst_errors: str, source_origin: types.SourceFileOrString, ignore_messages: t.Pattern[str] | None = None, ) -> types.YieldedLintError: """Parse rst errors and yield filtered :py:class:`rstcheck_core.types.LintError`. :param rst_errors: String with rst errors :param source_origin: Origin of the source with the errors :param ignore_messages: Regex for ignoring error messages; defaults to :py:obj:`None` :return: :py:obj:`None` :yield: Parsed and filtered :py:class:`rstcheck_core.types.LintError` s """ for message in rst_errors.splitlines(): with contextlib.suppress(ValueError): if ignore_messages and ignore_messages.search(message): continue yield _parse_gcc_style_error_message( message, source_origin=source_origin, has_column=False ) class _CheckWriter(docutils.writers.Writer): """Runs CheckTranslator on code blocks.""" def __init__( self, source: str, source_origin: types.SourceFileOrString, ignores: types.IgnoreDict | None = None, report_level: config.ReportLevel = config.DEFAULT_REPORT_LEVEL, *, warn_unknown_settings: bool = False, ) -> None: """Inititalize :py:class:`_CheckWriter`. :param source: Rst source to check :param source_origin: Path to file the source comes from :param ignores: Ignore information; defaults to :py:obj:`None` :param report_level: Report level; defaults to :py:data:`rstcheck_core.config.DEFAULT_REPORT_LEVEL` :param warn_unknown_settings: If a warning should be logged for unknown settings in config file; defaults to :py:obj:`False` """ docutils.writers.Writer.__init__(self) self.checkers: list[types.CheckerRunFunction] = [] self.source = source self.source_origin = source_origin self.ignores = ignores self.report_level = report_level self.warn_unknown_settings = warn_unknown_settings def translate(self) -> None: """Run CheckTranslator.""" visitor = _CheckTranslator( self.document, source=self.source, source_origin=self.source_origin, ignores=self.ignores, report_level=self.report_level, warn_unknown_settings=self.warn_unknown_settings, ) self.document.walkabout(visitor) self.checkers += visitor.checkers class _CheckTranslator(docutils.nodes.NodeVisitor): """Visits code blocks and checks for syntax errors in code.""" def __init__( # noqa: PLR0913 self, document: docutils.nodes.document, source: str, source_origin: types.SourceFileOrString, ignores: types.IgnoreDict | None = None, report_level: config.ReportLevel = config.DEFAULT_REPORT_LEVEL, *, warn_unknown_settings: bool = False, ) -> None: """Inititalize :py:class:`_CheckTranslator`. :param document: Document node :param source: Rst source to check :param source_origin: Path to file the source comes from :param ignores: Ignore information; defaults to :py:obj:`None` :param report_level: Report level; defaults to :py:data:`rstcheck_core.config.DEFAULT_REPORT_LEVEL` :param warn_unknown_settings: If a warning should be logged for unknown settings in config file; defaults to :py:obj:`False` """ docutils.nodes.NodeVisitor.__init__(self, document) self.checkers: list[types.CheckerRunFunction] = [] self.source = source self.source_origin = source_origin self.ignores = ignores or types.construct_ignore_dict() self.report_level = report_level self.warn_unknown_settings = warn_unknown_settings self.code_block_checker = CodeBlockChecker( source_origin, ignores, report_level, warn_unknown_settings=warn_unknown_settings ) self.code_block_ignore_lines = list( inline_config.find_code_block_ignore_lines( source=self.source, source_origin=self.source_origin, warn_unknown_settings=self.warn_unknown_settings, ) ) def visit_doctest_block(self, node: docutils.nodes.Element) -> None: """Add check for syntax of doctest. :param node: The doctest node """ if "doctest" in self.ignores["languages"]: return self._add_check( node=node, run=self.code_block_checker.create_checker(node.rawsource, "doctest"), language="doctest", is_code_node=False, ) def visit_literal_block(self, node: docutils.nodes.Element) -> None: """Add check for syntax of code block. :param node: The code block node :raises docutils.nodes.SkipNode: After a check was added or nothing to do """ # For "..code-block:: language" language = node.get("language") is_code_node = False if not language: # For "..code:: language" is_code_node = True classes = node.get("classes") if "code" not in classes: return language = classes[-1] directive_line = _get_code_block_directive_line(node, self.source) if directive_line is None: logger.warning( "Could not find line for literal block directive. " "This could be due to an indented code block. " "This message only warns about the missing line number " "and is no error of itself. " "Source: '%s'%s", self.source_origin, f" at line {node.line}" if node.line is not None else "", ) return if directive_line - 1 in self.code_block_ignore_lines: logger.debug( "Skipping code-block due to skip comment. Source: '%s' at line %s", self.source_origin, node.line, ) return if language in self.ignores["languages"]: return if language == "doctest" or ( language == "python" and node.rawsource.lstrip().startswith(">>> ") ): self.visit_doctest_block(node) raise docutils.nodes.SkipNode if self.code_block_checker.language_is_supported(language): run = self.code_block_checker.create_checker(node.rawsource, language) self._add_check(node=node, run=run, language=language, is_code_node=is_code_node) raise docutils.nodes.SkipNode def visit_paragraph(self, node: docutils.nodes.Element) -> None: """Check syntax of reStructuredText. :param node: The rst node """ find = MARKDOWN_LINK_REGEX.search(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: docutils.nodes.Element, run: types.CheckerRunFunction, language: str, *, is_code_node: bool, ) -> None: """Add node checker that will be run. :param node: The node to check :param run: The runner function that checks the node :param language: The language of the node :param is_code_node: If it is a code block node """ def run_check() -> types.YieldedLintError: """Yield found issues.""" all_results = run() if all_results is not None: if all_results: for result in all_results: error_offset = result["line_number"] - 1 line_number = getattr(node, "line", None) if line_number is not None: yield types.LintError( source_origin=result["source_origin"], line_number=_beginning_of_code_block( node=node, line_number=line_number, full_contents=self.source, is_code_node=is_code_node, ) + error_offset, message=f"({language}) {result['message']}", ) else: yield types.LintError( source_origin=self.source_origin, line_number=0, message="unknown error" ) self.checkers.append(run_check) def unknown_visit(self, node: docutils.nodes.Node) -> None: """Ignore.""" def unknown_departure(self, node: docutils.nodes.Node) -> None: """Ignore.""" def _beginning_of_code_block( node: docutils.nodes.Element, line_number: int, full_contents: str, *, is_code_node: bool ) -> int: """Get line number of beginning of code block. :param node: The code block node :param line_number: The current line number :param full_contents: The node's contents :param is_code_node: If it is a code block node :return: First fine number of the block """ if _extras.SPHINX_INSTALLED and not is_code_node: sphinx_code_block_delta = -1 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 lines = full_contents.splitlines() code_block_length = len(node.rawsource.splitlines()) with contextlib.suppress(IndexError): # Case where there are no extra spaces. if lines[line_number - 1].strip(): return line_number - code_block_length + 1 # The offsets are wrong if the RST text has multiple blank lines after # the code block. This is a workaround. for line_no in range(line_number, 1, -1): if lines[line_no - 2].strip(): break return line_no - code_block_length CODE_BLOCK_RE = re.compile(r"\.\. code::|\.\. code-block::|\.\. sourcecode::") def _get_code_block_directive_line(node: docutils.nodes.Element, full_contents: str) -> int | None: """Find line of code block directive. :param node: The code block node :param full_contents: The node's contents :return: Line of code block directive or :py:obj:`None` """ line_number = node.line if line_number is None: return None if _extras.SPHINX_INSTALLED: return line_number lines = full_contents.splitlines() for line_no in range(line_number, 1, -1): if CODE_BLOCK_RE.match(lines[line_no - 2].strip()) is not None: return line_no - 1 return None class CodeBlockChecker: """Checker for code blockes with different languages.""" def __init__( self, source_origin: types.SourceFileOrString, ignores: types.IgnoreDict | None = None, report_level: config.ReportLevel = config.DEFAULT_REPORT_LEVEL, *, warn_unknown_settings: bool = False, ) -> None: """Inititalize CodeBlockChecker. :param source_origin: Path to file the source comes from :param ignores: Ignore information; defaults to :py:obj:`None` :param report_level: Report level; defaults to :py:data:`rstcheck_core.config.DEFAULT_REPORT_LEVEL` :param warn_unknown_settings: If a warning should be logged for unknown settings in config file; defaults to :py:obj:`False` """ self.source_origin = source_origin self.ignores = ignores self.report_level = report_level self.warn_unknown_settings = warn_unknown_settings def language_is_supported(self, language: str) -> bool: """Check if given language can be checked. :param language: Language to check :return: If langauge can be checked """ return getattr(self, f"check_{language}", None) is not None def create_checker(self, source_code: str, language: str) -> types.CheckerRunFunction: """Create a checker function for the given source and language. :param source: Source code to check :param language: Language of the source code :return: Checker function """ return lambda: self.check(source_code, language) def check(self, source_code: str, language: str) -> types.YieldedLintError: """Call the appropiate checker function for the given langauge to check given source. :param source: Source code to check :param language: Language of the source code :return: :py:obj:`None` if language is not supported :yield: Found issues """ checker_function = t.Callable[[str], types.YieldedLintError] checker: checker_function | None = getattr(self, f"check_{language}", None) if checker is None: return None yield from checker(source_code) return None def check_python(self, source_code: str) -> types.YieldedLintError: """Check python source for syntax errors. :param source: Python source code to check :return: :py:obj:`None` :yield: Found issues """ logger.debug("Check python source.") try: with warnings.catch_warnings(): warnings.simplefilter("error", SyntaxWarning) compile(source_code, "", "exec") except SyntaxError as exception: yield types.LintError( source_origin=self.source_origin, line_number=int(exception.lineno or 0), message=exception.msg, ) def check_json(self, source_code: str) -> types.YieldedLintError: """Check JSON source for syntax errors. :param source: JSON source code to check :return: :py:obj:`None` :yield: Found issues """ logger.debug("Check JSON source.") try: json.loads(source_code) except ValueError as exception: message = f"{exception}" found = EXCEPTION_LINE_NO_REGEX.search(message) line_number = int(found.group(1)) if found else 0 yield types.LintError( source_origin=self.source_origin, line_number=int(line_number), message=message ) def check_yaml(self, source_code: str) -> types.YieldedLintError: """Check YAML source for syntax errors. :param source: JSON source code to check :return: :py:obj:`None` :yield: Found issues """ if not yaml_imported: logger.debug("PyYAML is not installed, ignoring YAML source.") return logger.debug("Check YAML source.") try: yaml.safe_load(source_code) except yaml.error.YAMLError as exception: message = f"{exception}" found = EXCEPTION_LINE_NO_REGEX.search(message) line_number = int(found.group(1)) if found else 0 yield types.LintError( source_origin=self.source_origin, line_number=int(line_number), message=message ) def check_xml(self, source_code: str) -> types.YieldedLintError: """Check XML source for syntax errors. :param source: XML source code to check :return: :py:obj:`None` :yield: Found issues """ logger.debug("Check XML source.") try: xml.etree.ElementTree.fromstring(source_code) # noqa: S314 except xml.etree.ElementTree.ParseError as exception: message = f"{exception}" found = EXCEPTION_LINE_NO_REGEX.search(message) line_number = int(found.group(1)) if found else 0 yield types.LintError( source_origin=self.source_origin, line_number=int(line_number), message=message ) def check_rst(self, source_code: str) -> types.YieldedLintError: """Check nested rst source for syntax errors. :param source: rst source code to check :return: :py:obj:`None` :yield: Found issues """ logger.debug("Check RST source.") yield from check_source( source_code, source_file=self.source_origin, ignores=self.ignores, report_level=self.report_level, warn_unknown_settings=self.warn_unknown_settings, ) def check_doctest(self, source_code: str) -> types.YieldedLintError: """Check doctest source for 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. :param source: XML source code to check :return: :py:obj:`None` :yield: Found issues """ logger.debug("Check doctest source.") parser = doctest.DocTestParser() try: parser.parse(source_code) except ValueError as exception: message = f"{exception}" match = DOCTEST_LINE_NO_REGEX.match(message) if match: yield types.LintError( source_origin=self.source_origin, line_number=int(match.group(1)), message=message, ) def check_bash(self, source_code: str) -> types.YieldedLintError: """Check bash source for syntax errors. :param source: bash source code to check :return: :py:obj:`None` :yield: Found issues """ logger.debug("Check bash source.") result = self._run_in_subprocess(source_code, ".bash", ["bash", "-n"]) if result: (output, filename) = result prefix = str(filename) + ": line " for line in output.splitlines(): if not line.startswith(prefix): # pragma: no cover # NOTE: Case not reproducible continue message = line[len(prefix) :] split_message = message.split(":", 1) yield types.LintError( source_origin=self.source_origin, line_number=int(split_message[0]) - 1, message=split_message[1].strip(), ) def check_c(self, source_code: str) -> types.YieldedLintError: """Check C source for syntax errors. :param source: C source code to check :return: :py:obj:`None` :yield: Found issues """ logger.debug("Check C source.") return self._gcc_checker( source_code, ".c", [ os.getenv("CC", "gcc"), *shlex.split(os.getenv("CFLAGS", "")), *shlex.split(os.getenv("CPPFLAGS", "")), "-I.", "-I..", ], ) def check_cpp(self, source_code: str) -> types.YieldedLintError: """Check C++ source for syntax errors. :param source: C++ source code to check :return: :py:obj:`None` :yield: Found issues """ logger.debug("Check C++ source.") yield from self._gcc_checker( # Add a newline to ignore "no newline at end of file" errors # that are reported using clang (e.g. on macOS). source_code + "\n", ".cpp", [ os.getenv("CXX", "g++"), *shlex.split(os.getenv("CXXFLAGS", "")), *shlex.split(os.getenv("CPPFLAGS", "")), "-I.", "-I..", ], ) def _gcc_checker( self, source_code: str, filename_suffix: str, arguments: list[str] ) -> types.YieldedLintError: """Check code blockes using gcc (Helper function). :param source_code: Source code to check :param filename_suffix: File suffix for language of the source code :param arguments: Command and arguments to run :return: :py:obj:`None` :yield: Found issues """ result = self._run_in_subprocess( source_code, filename_suffix, [*arguments, "-pedantic", "-fsyntax-only"] ) if result: (output, temp_file_name) = result for line in output.splitlines(): try: yield _parse_gcc_style_error_message( line, source_origin=self.source_origin, temp_file_name=temp_file_name ) except ValueError: continue def _run_in_subprocess( self, code: str, filename_suffix: str, arguments: list[str], ) -> tuple[str, pathlib.Path] | None: """Run checker in a subprocess (Helper function). :param source_code: Source code to check :param filename_suffix: File suffix for language of the source code :param arguments: Command and arguments to run :return: :py:obj:`None` if no issues were found else a tuple of the stderr and temp-file name """ encoding = locale.getpreferredencoding() or sys.getdefaultencoding() source_origin_path = self.source_origin if isinstance(source_origin_path, str): source_origin_path = pathlib.Path(source_origin_path) # NOTE: On windows a file cannot be opened twice. # Therefore close it before using it in subprocess. temporary_file = tempfile.NamedTemporaryFile( mode="wb", suffix=filename_suffix, delete=False ) temporary_file_path = pathlib.Path(temporary_file.name) try: temporary_file.write(code.encode("utf-8")) temporary_file.flush() temporary_file.close() subprocess.run( [*arguments, temporary_file.name], # noqa: S603 capture_output=True, cwd=source_origin_path.parent, check=True, ) except subprocess.CalledProcessError as exc: return (exc.stderr.decode(encoding), temporary_file_path) else: return None finally: temporary_file_path.unlink() def _parse_gcc_style_error_message( message: str, source_origin: types.SourceFileOrString, *, temp_file_name: pathlib.Path | None = None, has_column: bool = True, ) -> types.LintError: """Parse GCC-style error message. Return (line_number, message). Raise ValueError if message cannot be parsed. :param message: Message to parse :param filename: File the code block producing the errors comes from :param temp_file_name: File the message is associated with :param has_column: The the message has a column number; defaults to :py:obj:`True` :raises ValueError: If the message cannot be parsed :return: Parsed message """ colons = 2 if has_column else 1 prefix = str(temp_file_name or source_origin) + ":" if not message.startswith(prefix): logger.debug("Skipping unparsable message: '%s'.", message) msg = "Message cannot be parsed." raise ValueError(msg) message = message[len(prefix) :] split_message = message.split(":", colons) line_number = int(split_message[0]) return types.LintError( source_origin=source_origin, line_number=line_number, message=split_message[colons].strip() ) rstcheck-core-1.2.1/src/rstcheck_core/config.py000066400000000000000000000521241457751767200215240ustar00rootroot00000000000000"""Rstcheck configuration functionality.""" from __future__ import annotations import configparser import contextlib import enum import logging import pathlib import re import typing as t import pydantic from . import _extras tomllib_imported = False try: import tomllib tomllib_imported = True except ModuleNotFoundError: if _extras.TOMLI_INSTALLED: # pragma: no cover import tomli as tomllib # type: ignore[import-not-found,no-redef] logger = logging.getLogger(__name__) CONFIG_FILES = [".rstcheck.cfg", "setup.cfg"] """Supported default config files.""" if _extras.TOMLI_INSTALLED or tomllib_imported: # pragma: no cover CONFIG_FILES = [".rstcheck.cfg", "pyproject.toml", "setup.cfg"] class ReportLevel(enum.Enum): """Report levels supported by docutils.""" INFO = 1 WARNING = 2 ERROR = 3 SEVERE = 4 NONE = 5 ReportLevelMap = { "info": 1, "warning": 2, "error": 3, "severe": 4, "none": 5, } """Map docutils report levels in text form to numbers.""" DEFAULT_REPORT_LEVEL = ReportLevel.INFO """Default report level.""" def _split_str_validator(value: t.Any) -> list[str] | None: # noqa: ANN401 """Validate and parse strings and string-lists. Comma separated strings are split into a list. :param value: Value to validate :raises ValueError: If not a :py:class:`str` or :py:class:`list` of :py:class:`str` :return: List of strings """ if value is None: return None if isinstance(value, str): return [v.strip() for v in value.split(",") if v.strip()] if isinstance(value, list) and all(isinstance(v, str) for v in value): return [v.strip() for v in value if v.strip()] msg = "Not a string or list of strings" raise TypeError(msg) class RstcheckConfigFile(pydantic.BaseModel): """Rstcheck config file. :raises ValueError: If setting has incorrect value or type :raises pydantic.ValidationError: If setting is not parsable into correct type """ report_level: t.Optional[ReportLevel] = None # noqa: UP007 ignore_directives: t.Optional[t.List[str]] = None # noqa: UP007,UP006 ignore_roles: t.Optional[t.List[str]] = None # noqa: UP007,UP006 ignore_substitutions: t.Optional[t.List[str]] = None # noqa: UP007,UP006 ignore_languages: t.Optional[t.List[str]] = None # noqa: UP007,UP006 ignore_messages: t.Optional[t.Pattern[str]] = None # noqa: UP007 @pydantic.field_validator("report_level", mode="before") @classmethod def valid_report_level(cls, value: t.Any) -> ReportLevel | None: # noqa: ANN401 """Validate the report_level setting. :param value: Value to validate :raises ValueError: If ``value`` is not a valid docutils report level :return: Instance of :py:class:`ReportLevel` or None if emptry string. """ if value is None: return None if isinstance(value, ReportLevel): return value if value == "": return DEFAULT_REPORT_LEVEL if isinstance(value, bool): msg = "Invalid report level" raise TypeError(msg) if isinstance(value, str): if value.casefold() in set(ReportLevelMap): return ReportLevel(ReportLevelMap[value.casefold()]) with contextlib.suppress(ValueError): value = int(value) max_report_lvl = 5 min_report_lvl = 1 if isinstance(value, int) and min_report_lvl <= value <= max_report_lvl: return ReportLevel(value) msg = "Invalid report level" raise TypeError(msg) @pydantic.field_validator( "ignore_directives", "ignore_roles", "ignore_substitutions", "ignore_languages", mode="before", ) @classmethod def split_str(cls, value: t.Any) -> list[str] | None: # noqa: ANN401 """Validate and parse the following ignore_* settings. - ignore_directives - ignore_roles - ignore_substitutions - ignore_languages Comma separated strings are split into a list. :param value: Value to validate :raises ValueError: If not a :py:class:`str` or :py:class:`list` of :py:class:`str` :return: List of things to ignore in the respective category """ return _split_str_validator(value) @pydantic.field_validator("ignore_messages", mode="before") @classmethod def join_regex_str(cls, value: t.Any) -> str | t.Pattern[str] | None: # noqa: ANN401 """Validate and concatenate the ignore_messages setting to a RegEx string. If a list ist given, the entries are concatenated with "|" to create an or RegEx. :param value: Value to validate :raises ValueError: If not a :py:class:`str` or :py:class:`list` of :py:class:`str` :return: A RegEx string with messages to ignore or :py:class:`typing.Pattern` if it is one already """ if value is None: return None if isinstance(value, re.Pattern): return value if isinstance(value, list) and all(isinstance(v, str) for v in value): return r"|".join(value) if isinstance(value, str): return value msg = "Not a string or list of strings" raise TypeError(msg) class RstcheckConfig(RstcheckConfigFile): """Rstcheck config. :raises ValueError: If setting has incorrect value or type :raises pydantic.ValidationError: If setting is not parsable into correct type """ config_path: t.Optional[pathlib.Path] = None # noqa: UP007 recursive: t.Optional[bool] = None # noqa: UP007 warn_unknown_settings: t.Optional[bool] = None # noqa: UP007 class _RstcheckConfigINIFile(pydantic.BaseModel): """Type for [rstcheck] section in INI file. The types apply to the file's data before the parsing by :py:class:`RstcheckConfig` is done. :raises pydantic.ValidationError: If setting is not parsable into correct type """ report_level: t.Union[str, int, None] = None # noqa: UP007 ignore_directives: t.Optional[str] = None # noqa: UP007 ignore_roles: t.Optional[str] = None # noqa: UP007 ignore_substitutions: t.Optional[str] = None # noqa: UP007 ignore_languages: t.Optional[str] = None # noqa: UP007 ignore_messages: t.Optional[str] = None # noqa: UP007 def _load_config_from_ini_file( ini_file: pathlib.Path, *, log_missing_section_as_warning: bool = True, warn_unknown_settings: bool = False, ) -> RstcheckConfigFile | None: """Load, parse and validate rstcheck config from a ini file. :param ini_file: INI file to load config from :param log_missing_section_as_warning: If a missing [tool.rstcheck] section should be logged at WARNING (:py:obj:`True`) or ``INFO`` (:py:obj:`False`) level; defaults to :py:obj:`True` :param warn_unknown_settings: If a warning should be logged for unknown settings in config file; defaults to :py:obj:`False` :raises FileNotFoundError: If the file is not found :return: instance of :py:class:`RstcheckConfigFile` or :py:class:`None` on missing config section or ``NONE`` is passed as the config path. """ logger.debug("Try loading config from INI file: '%s'", ini_file) if ini_file.name == "NONE": logger.info("Config path is set to 'NONE'. No config file is loaded.") return None resolved_file = ini_file.resolve() if not resolved_file.is_file(): msg = f"{resolved_file}" raise FileNotFoundError(msg) parser = configparser.ConfigParser() parser.read(resolved_file) if not parser.has_section("rstcheck"): if log_missing_section_as_warning: logger.warning( "Config file has no [rstcheck] section: '%s'.", ini_file, ) return None logger.info( "Config file has no [rstcheck] section: '%s'.", ini_file, ) return None config_values_raw = dict(parser.items("rstcheck")) if warn_unknown_settings: known_settings = _RstcheckConfigINIFile().model_dump().keys() unknown = [s for s in config_values_raw if s not in known_settings] if unknown: logger.warning( "Unknown setting(s) %s found in file: '%s'.", unknown, ini_file, ) config_values_checked = _RstcheckConfigINIFile(**config_values_raw) return RstcheckConfigFile(**config_values_checked.model_dump()) class _RstcheckConfigTOMLFile( pydantic.BaseModel, ): """Type for [tool.rstcheck] section in TOML file. The types apply to the file's data before the parsing by :py:class:`RstcheckConfig` is done. :raises pydantic.ValidationError: If setting is not parsable into correct type """ report_level: t.Union[str, int, None] = None # noqa: UP007 ignore_directives: t.Optional[t.List[str]] = None # noqa: UP006, UP007 ignore_roles: t.Optional[t.List[str]] = None # noqa: UP006, UP007 ignore_substitutions: t.Optional[t.List[str]] = None # noqa: UP006, UP007 ignore_languages: t.Optional[t.List[str]] = None # noqa: UP006, UP007 ignore_messages: t.Union[t.List[str], str, None] = None # noqa: UP006, UP007 def _load_config_from_toml_file( toml_file: pathlib.Path, *, log_missing_section_as_warning: bool = True, warn_unknown_settings: bool = False, ) -> RstcheckConfigFile | None: """Load, parse and validate rstcheck config from a TOML file. .. warning:: Needs tomli installed for python versions before 3.11! Use toml extra. :param toml_file: TOML file to load config from :param log_missing_section_as_warning: If a missing [tool.rstcheck] section should be logged at WARNING (:py:obj:`True`) or ``INFO`` (:py:obj:`False`) level; defaults to :py:obj:`True` :param warn_unknown_settings: If a warning should be logged for unknown settings in config file; defaults to :py:obj:`False` :raises ValueError: If the file is not a TOML file :raises FileNotFoundError: If the file is not found :return: instance of :py:class:`RstcheckConfigFile` or :py:obj:`None` on missing config section or ``NONE`` is passed as the config path. """ _extras.install_guard_tomli(tomllib_imported=tomllib_imported) logger.debug("Try loading config from TOML file: '%s'.", toml_file) if toml_file.name == "NONE": logger.info("Config path is set to 'NONE'. No config file is loaded.") return None resolved_file = toml_file.resolve() if not resolved_file.is_file(): logging.error("Config file is not a file: '%s'.", toml_file) msg = f"{resolved_file}" raise FileNotFoundError(msg) if resolved_file.suffix.casefold() != ".toml": logging.error("Config file is not a TOML file: '%s'.", toml_file) msg = "File is not a TOML file" raise ValueError(msg) with pathlib.Path(resolved_file).open("rb") as toml_file_handle: toml_dict = tomllib.load(toml_file_handle) rstcheck_section: t.Optional[dict[str, t.Any]] = toml_dict.get("tool", {}).get( # noqa: UP007 "rstcheck" ) if rstcheck_section is None: if log_missing_section_as_warning: logger.warning( "Config file has no [tool.rstcheck] section: '%s'.", toml_file, ) return None logger.info( "Config file has no [tool.rstcheck] section: '%s'.", toml_file, ) return None if warn_unknown_settings: known_settings = _RstcheckConfigTOMLFile().model_dump().keys() unknown = [s for s in rstcheck_section if s not in known_settings] if unknown: logger.warning( "Unknown setting(s) %s found in file: '%s'.", unknown, toml_file, ) config_values_checked = _RstcheckConfigTOMLFile(**rstcheck_section) return RstcheckConfigFile(**config_values_checked.model_dump()) def load_config_file( file_path: pathlib.Path, *, log_missing_section_as_warning: bool = True, warn_unknown_settings: bool = False, ) -> RstcheckConfigFile | None: """Load, parse and validate rstcheck config from a file. .. caution:: If a TOML file is passed this function need tomli installed for python versions before 3.11! Use toml extra or install manually. :param file_path: File to load config from :param log_missing_section_as_warning: If a missing config section should be logged at WARNING (:py:obj:`True`) or ``INFO`` (:py:obj:`False`) level; defaults to :py:obj:`True` :param warn_unknown_settings: If a warning should be logged for unknown settings in config file; defaults to :py:obj:`False` :raises FileNotFoundError: If the file is not found :return: instance of :py:class:`RstcheckConfigFile` or :py:obj:`None` on missing config section or ``NONE`` is passed as the config path. """ logger.debug("Try loading config file.") if file_path.name == "NONE": logger.info("Config path is set to 'NONE'. No config file is loaded.") return None if file_path.suffix.casefold() == ".toml": return _load_config_from_toml_file( file_path, log_missing_section_as_warning=log_missing_section_as_warning, warn_unknown_settings=warn_unknown_settings, ) return _load_config_from_ini_file( file_path, log_missing_section_as_warning=log_missing_section_as_warning, warn_unknown_settings=warn_unknown_settings, ) def load_config_file_from_dir( dir_path: pathlib.Path, *, log_missing_section_as_warning: bool = False, warn_unknown_settings: bool = False, ) -> RstcheckConfigFile | None: """Search, load, parse and validate rstcheck config from a directory. Searches files from :py:data:`CONFIG_FILES` in the directory. If a file is found, try to load the config from it. If is has no config, search further. :param dir_path: Directory to search :param log_missing_section_as_warning: If a missing config section in a config file should be logged at WARNING (:py:obj:`True`) or ``INFO`` (:py:obj:`False`) level; defaults to :py:obj:`False` :param warn_unknown_settings: If a warning should be logged for unknown settings in config file; defaults to :py:obj:`False` :return: instance of :py:class:`RstcheckConfigFile` or :py:obj:`None` if no file is found or no file has a rstcheck section or ``NONE`` is passed as the config path. """ logger.debug("Try loading config file from directory: '%s'.", dir_path) if dir_path.name == "NONE": logger.info("Config path is set to 'NONE'. No config file is loaded.") return None config = None for file_name in CONFIG_FILES: file_path = (dir_path / file_name).resolve() if file_path.is_file(): config = load_config_file( file_path, log_missing_section_as_warning=( log_missing_section_as_warning or (file_name == ".rstcheck.cfg") ), warn_unknown_settings=warn_unknown_settings, ) if config is not None: break if config is None: logger.info( "No config section in supported config files found in directory: '%s'.", dir_path, ) return config def load_config_file_from_dir_tree( dir_path: pathlib.Path, *, log_missing_section_as_warning: bool = False, warn_unknown_settings: bool = False, ) -> RstcheckConfigFile | None: """Search, load, parse and validate rstcheck config from a directory tree. Searches files from :py:data:`CONFIG_FILES` in the directory. If a file is found, try to load the config from it. If is has no config, search further. If no config is found in the directory search its parents one by one. :param dir_path: Directory to search :param log_missing_section_as_warning: If a missing config section in a config file should be logged at ``WARNING`` (:py:obj:`True`) or ``INFO`` (:py:obj:`False`) level; defaults to :py:obj:`False` :param warn_unknown_settings: If a warning should be logged for unknown settings in config file; defaults to :py:obj:`False` :return: instance of :py:class:`RstcheckConfigFile` or :py:obj:`None` if no file is found or no file has a rstcheck section or ``NONE`` is passed as the config path. """ logger.debug("Try loading config file from directory tree: '%s'.", dir_path) if dir_path.name == "NONE": logger.info("Config path is set to 'NONE'. No config file is loaded.") return None config = None search_dir = dir_path.resolve() while True: config = load_config_file_from_dir( search_dir, log_missing_section_as_warning=log_missing_section_as_warning, warn_unknown_settings=warn_unknown_settings, ) if config is not None: break parent_dir = search_dir.parent.resolve() if parent_dir == search_dir: break search_dir = parent_dir if config is None: logger.info( "No config section in supported config files found in directory tree: '%s'.", dir_path, ) return config def load_config_file_from_path( path: pathlib.Path, *, search_dir_tree: bool = False, log_missing_section_as_warning_for_file: bool = True, log_missing_section_as_warning_for_dir: bool = False, warn_unknown_settings: bool = False, ) -> RstcheckConfigFile | None: """Analyse the path and call the correct config file loader. :param path: Path to load config file from; can be a file or directory :param search_dir_tree: If the directory tree should be searched; only applies if ``path`` is a directory; defaults to :py:obj:`False` :param log_missing_section_as_warning_for_file: If a missing config section in a config file should be logged at WARNING (:py:obj:`True`) or ``INFO`` (:py:obj:`False`) level when the given path is a file; defaults to :py:obj:`True` :param log_missing_section_as_warning_for_dir: If a missing config section in a config file should be logged at ``WARNING`` (:py:obj:`True`) or ``INFO`` (:py:obj:`False`) level when the given file is a direcotry; defaults to :py:obj:`False` :param warn_unknown_settings: If a warning should be logged for unknown settings in config file; defaults to :py:obj:`False` :raises FileNotFoundError: When the passed path is not found. :return: instance of :py:class:`RstcheckConfigFile` or :py:obj:`None` if no file is found or no file has a rstcheck section or ``NONE`` is passed as the config path. """ logger.debug("Try loading config file from path: '%s'.", path) if path.name == "NONE": logger.info("Config path is set to 'NONE'. No config file is loaded.") return None resolved_path = path.resolve() if resolved_path.is_file(): return load_config_file( resolved_path, log_missing_section_as_warning=log_missing_section_as_warning_for_file, warn_unknown_settings=warn_unknown_settings, ) if resolved_path.is_dir(): if search_dir_tree: return load_config_file_from_dir_tree( resolved_path, log_missing_section_as_warning=log_missing_section_as_warning_for_dir, warn_unknown_settings=warn_unknown_settings, ) return load_config_file_from_dir( resolved_path, log_missing_section_as_warning=log_missing_section_as_warning_for_dir, warn_unknown_settings=warn_unknown_settings, ) raise FileNotFoundError(2, "Passed config path not found.", path) def merge_configs( config_base: RstcheckConfig, config_add: RstcheckConfig | RstcheckConfigFile, *, config_add_is_dominant: bool = True, ) -> RstcheckConfig: """Merge two configs into a new one. :param config_base: The base config to merge into :param config_add: The config that is merged into the ``config_base`` :param config_add_is_dominant: If the ``config_add`` overwrites values of ``config_base``; defaults to :py:obj:`True` :return: New merged config """ logger.debug("Merging configs.") sub_config: RstcheckConfig | RstcheckConfigFile = config_base sub_config_dict = sub_config.model_dump() for setting in dict(sub_config_dict): if sub_config_dict[setting] is None: del sub_config_dict[setting] dom_config: RstcheckConfig | RstcheckConfigFile = config_add dom_config_dict = dom_config.model_dump() for setting in dict(dom_config_dict): if dom_config_dict[setting] is None: del dom_config_dict[setting] if config_add_is_dominant is False: sub_config_dict, dom_config_dict = dom_config_dict, sub_config_dict merged_config_dict = {**sub_config_dict, **dom_config_dict} return RstcheckConfig(**merged_config_dict) rstcheck-core-1.2.1/src/rstcheck_core/inline_config.py000066400000000000000000000217121457751767200230610ustar00rootroot00000000000000"""Inline config comment functionality.""" from __future__ import annotations import functools import logging import re import typing as t from . import types logger = logging.getLogger(__name__) RSTCHECK_CONFIG_COMMENT_REGEX = re.compile(r"\.\. rstcheck: (.*)=(.*)$") VALID_INLINE_CONFIG_KEYS = ( "ignore-directives", "ignore-roles", "ignore-substitutions", "ignore-languages", ) ValidInlineConfigKeys = t.Literal[ "ignore-directives", "ignore-roles", "ignore-substitutions", "ignore-languages" ] RSTCHECK_FLOW_CONTROL_COMMENT_REGEX = re.compile(r"\.\. rstcheck: ([a-z-]*)$") VALID_INLINE_FLOW_CONTROLS = ("ignore-next-code-block",) @functools.lru_cache def get_inline_config_from_source( source: str, source_origin: types.SourceFileOrString, *, warn_unknown_settings: bool = False ) -> list[types.InlineConfig]: """Get rstcheck inline configs from source. Unknown configs are ignored. :param source: Source to get config from :param source_origin: Origin of the source with the inline ignore comments :param warn_unknown_settings: If a warning should be logged on unknown settings; defaults to :py:obj:`False` :return: A list of inline configs """ configs: list[types.InlineConfig] = [] for idx, line in enumerate(source.splitlines()): match = RSTCHECK_CONFIG_COMMENT_REGEX.search(line) if match is None: continue key = match.group(1).strip() value = match.group(2).strip() if key not in VALID_INLINE_CONFIG_KEYS: if warn_unknown_settings: logger.warning( "Unknown inline config '%s' found. Source: '%s' at line %s", key, source_origin, idx + 1, ) continue configs.append(types.InlineConfig(key=key, value=value)) return configs def _filter_config_and_split_values( target_config: ValidInlineConfigKeys, source: str, source_origin: types.SourceFileOrString, *, warn_unknown_settings: bool = False, ) -> t.Generator[str, None, None]: """Get specified configs and comma split them. :param target_config: Config target to filter for :param source: Source to get config from :param source_origin: Origin of the source with the inline ignore comments :param warn_unknown_settings: If a warning should be logged on unknown settings; defaults to :py:obj:`False` :return: None :yield: Single values for the ``target_config`` """ inline_configs = get_inline_config_from_source( source, source_origin, warn_unknown_settings=warn_unknown_settings ) for inline_config in inline_configs: if inline_config["key"] == target_config: for language in inline_config["value"].split(","): yield language.strip() def find_ignored_directives( source: str, source_origin: types.SourceFileOrString, *, warn_unknown_settings: bool = False ) -> t.Generator[str, None, None]: """Search the rst source for rstcheck inline ignore-directives comments. Directives are ignored via comment. For example, to ignore directive1, directive2, and directive3: .. testsetup:: from rstcheck_core.inline_config import find_ignored_directives >>> list(find_ignored_directives(''' ... Example ... ======= ... ... .. rstcheck: ignore-directives=directive1,directive3 ... ... .. rstcheck: ignore-directives=directive2 ... ''', "")) ['directive1', 'directive3', 'directive2'] :param source: Rst source code :param source_origin: Origin of the source with the inline ignore comments :return: None :yield: Found directives to ignore """ yield from _filter_config_and_split_values( "ignore-directives", source, source_origin, warn_unknown_settings=warn_unknown_settings ) def find_ignored_roles( source: str, source_origin: types.SourceFileOrString, *, warn_unknown_settings: bool = False ) -> t.Generator[str, None, None]: """Search the rst source for rstcheck inline ignore-roles comments. Roles are ignored via comment. For example, to ignore role1, role2, and role3: .. testsetup:: from rstcheck_core.inline_config import find_ignored_roles >>> list(find_ignored_roles(''' ... Example ... ======= ... ... .. rstcheck: ignore-roles=role1,role3 ... ... .. rstcheck: ignore-roles=role2 ... ''', "")) ['role1', 'role3', 'role2'] :param source: Rst source code :param source_origin: Origin of the source with the inline ignore comments :return: None :yield: Found roles to ignore """ yield from _filter_config_and_split_values( "ignore-roles", source, source_origin, warn_unknown_settings=warn_unknown_settings ) def find_ignored_substitutions( source: str, source_origin: types.SourceFileOrString, *, warn_unknown_settings: bool = False ) -> t.Generator[str, None, None]: """Search the rst source for rstcheck inline ignore-substitutions comments. Substitutions are ignored via comment. For example, to ignore substitution1, substitution2, and substitution3: .. testsetup:: from rstcheck_core.inline_config import find_ignored_substitutions >>> list(find_ignored_substitutions(''' ... Example ... ======= ... ... .. rstcheck: ignore-substitutions=substitution1,substitution3 ... ... .. rstcheck: ignore-substitutions=substitution2 ... ''', "")) ['substitution1', 'substitution3', 'substitution2'] :param source: Rst source code :param source_origin: Origin of the source with the inline ignore comments :return: None :yield: Found substitutions to ignore """ yield from _filter_config_and_split_values( "ignore-substitutions", source, source_origin, warn_unknown_settings=warn_unknown_settings ) def find_ignored_languages( source: str, source_origin: types.SourceFileOrString, *, warn_unknown_settings: bool = False ) -> t.Generator[str, None, None]: """Search the rst source for rstcheck inline ignore-languages comments. Languages are ignored via comment. For example, to ignore C++, JSON, and Python: .. testsetup:: from rstcheck_core.inline_config import find_ignored_languages >>> list(find_ignored_languages(''' ... Example ... ======= ... ... .. rstcheck: ignore-languages=cpp,json ... ... .. rstcheck: ignore-languages=python ... ''', "")) ['cpp', 'json', 'python'] :param source: Rst source code :param source_origin: Origin of the source with the inline ignore comments :return: None :yield: Found languages to ignore """ yield from _filter_config_and_split_values( "ignore-languages", source, source_origin, warn_unknown_settings=warn_unknown_settings ) @functools.lru_cache def get_inline_flow_control_from_source( source: str, source_origin: types.SourceFileOrString, *, warn_unknown_settings: bool = False ) -> list[types.InlineFlowControl]: """Get rstcheck inline flow control from source. Unknown flow controls are ignored. :param source: Source to get config from :param source_origin: Origin of the source with the inline flow control :param warn_unknown_settings: If a warning should be logged on unknown settings; defaults to :py:obj:`False` :return: A list of inline flow controls """ configs: list[types.InlineFlowControl] = [] for idx, line in enumerate(source.splitlines()): match = RSTCHECK_FLOW_CONTROL_COMMENT_REGEX.search(line) if match is None: continue value = match.group(1).strip() line_number = idx + 1 if value not in VALID_INLINE_FLOW_CONTROLS: if warn_unknown_settings: logger.warning( "Unknown inline flow control '%s' found. Source: '%s' at line %s", value, source_origin, line_number, ) continue configs.append(types.InlineFlowControl(value=value, line_number=line_number)) return configs def find_code_block_ignore_lines( source: str, source_origin: types.SourceFileOrString, *, warn_unknown_settings: bool = False, ) -> t.Generator[int, None, None]: """Get lines of ``ignore-next-code-block`` flow control comments. :param source: Source to get config from :param source_origin: Origin of the source with the inline ignore comments :param warn_unknown_settings: If a warning should be logged on unknown settings; defaults to :py:obj:`False` :return: None :yield: Single values for the ``target_config`` """ flow_controls = get_inline_flow_control_from_source( source, source_origin, warn_unknown_settings=warn_unknown_settings ) for flow_control in flow_controls: if flow_control["value"] == "ignore-next-code-block": yield flow_control["line_number"] rstcheck-core-1.2.1/src/rstcheck_core/py.typed000066400000000000000000000000001457751767200213660ustar00rootroot00000000000000rstcheck-core-1.2.1/src/rstcheck_core/runner.py000066400000000000000000000223411457751767200215660ustar00rootroot00000000000000"""Runner of rstcheck_core.""" from __future__ import annotations import logging import multiprocessing import os import pathlib import re import sys import typing as t from . import _sphinx, checker, config, types logger = logging.getLogger(__name__) class RstcheckMainRunner: """Main runner of rstcheck_core.""" def __init__( self, check_paths: list[pathlib.Path], rstcheck_config: config.RstcheckConfig, *, overwrite_config: bool = True, ) -> None: """Initialize the :py:class:`RstcheckMainRunner` with a base config. :param check_paths: Files to check. :param rstcheck_config: Base configuration config from e.g. the CLI. :param overwrite_config: If file config overwrites current config; defaults to True """ self.config = rstcheck_config self.overwrite_config = overwrite_config if rstcheck_config.config_path: self.load_config_file( rstcheck_config.config_path, warn_unknown_settings=rstcheck_config.warn_unknown_settings or False, ) self.check_paths = check_paths self._files_to_check: list[pathlib.Path] = [] self._nonexisting_paths: list[pathlib.Path] = [] self.update_file_list() pool_size = multiprocessing.cpu_count() # NOTE: Work around https://bugs.python.org/issue45077 self._pool_size = pool_size if sys.platform != "win32" else min(pool_size, 61) self.errors: list[types.LintError] = [] @property def files_to_check(self) -> list[pathlib.Path]: """List of files to check. This list is updated via the :py:meth:`RstcheckMainRunner.update_file_list` method. """ return self._files_to_check @property def nonexisting_paths(self) -> list[pathlib.Path]: """List of paths which do not exist. This list is updated via the :py:meth:`RstcheckMainRunner.update_file_list` method. """ return self._nonexisting_paths def load_config_file( self, config_path: pathlib.Path, *, warn_unknown_settings: bool = False ) -> None: """Load config from file and merge with current config. If the loaded file config overwrites the current config depends on the ``self.overwrite_config`` attribute set on initialization. :param config_path: Path to config file; can be directory or file :param warn_unknown_settings: If a warning should be logged for unknown settings in config file; defaults to :py:obj:`False` """ logger.info("Load config file for main runner: '%s'.", config_path) file_config = config.load_config_file_from_path( config_path, warn_unknown_settings=warn_unknown_settings ) if file_config is None: logger.warning("Config file was empty or not found.") return logger.debug( "Merging config from file into main config. File config is dominant: %s", self.overwrite_config, ) self.config = config.merge_configs( self.config, file_config, config_add_is_dominant=self.overwrite_config ) def update_file_list(self) -> None: """Update file path list with paths specified on initialization. Uses paths from ``RstcheckMainRunner.check_paths``, resolves all file paths and saves them in :py:attr:`RstcheckMainRunner.files_to_check`. If a given path does not exist, it is filtered out and saved in :py:attr:`RstcheckMainRunner.files_to_check`. Clear the current file list. Then get the file and directory paths specified with ``self.check_paths`` attribute set on initialization and search them for rst files to check. Add those files to the file list. """ logger.debug("Updating list of files to check.") paths = list(self.check_paths) self._files_to_check = [] if len(paths) == 1 and paths[0].name == "-": logger.info("'-' detected. Using stdin for input.'") self._files_to_check.append(paths[0]) return paths = self._filter_nonexisting_paths(paths) def checkable_rst_file(f: pathlib.Path) -> bool: return f.is_file() and not f.name.startswith(".") and f.suffix.casefold() == ".rst" while paths: path = paths.pop(0) resolved_path = path.resolve() if self.config.recursive and resolved_path.is_dir(): for root, directories, children in os.walk(path): root_path = pathlib.Path(root) paths += [ root_path / f for f in children if checkable_rst_file((root_path / f).resolve()) ] directories[:] = [d for d in directories if not d.startswith(".")] continue if checkable_rst_file(resolved_path): self._files_to_check.append(path) def _filter_nonexisting_paths(self, paths: list[pathlib.Path]) -> list[pathlib.Path]: """Filter nonexisting paths out. If recursive is not active only files are allowed, else directories are also allowed. :param paths: List of paths to filter :return: Filtered path list """ self._nonexisting_paths = [] _paths = list(paths) for path in _paths: resolved_path = path.resolve() if resolved_path.is_file(): continue if self.config.recursive and resolved_path.is_dir(): continue _paths.remove(path) self._nonexisting_paths.append(path) if self.config.recursive: logger.warning( "Path does not exist or is neither a file nor a directory: '%s'.", path, ) continue logger.warning( "Path does not exist or is not a file: '%s'.", path, ) return _paths def _run_checks_sync(self) -> list[list[types.LintError]]: """Check all files from the file list syncronously and return the errors. :return: List of lists of errors found per file """ logger.debug("Runnning checks synchronically.") with _sphinx.load_sphinx_if_available(): return [ checker.check_file(file, self.config, self.overwrite_config) for file in self._files_to_check ] def _run_checks_parallel(self) -> list[list[types.LintError]]: """Check all files from the file list in parallel and return the errors. :return: List of lists of errors found per file """ logger.debug( "Runnning checks in parallel with pool size of %s.", self._pool_size, ) with _sphinx.load_sphinx_if_available(), multiprocessing.Pool(self._pool_size) as pool: return pool.starmap( checker.check_file, [(file, self.config, self.overwrite_config) for file in self._files_to_check], ) def _update_results(self, results: list[list[types.LintError]]) -> None: """Take results and update error cache. Result normally come from :py:meth:`RstcheckMainRunner._run_checks_sync` or :py:meth:`RstcheckMainRunner._run_checks_parallel`. :param results: List of lists of errors found """ self.errors = [] for errors in results: self.errors += errors def check(self) -> None: """Check all files in the file list and save the errors. Multiple files are run in parallel. A new call overwrite the old cached errors. """ logger.info("Run checks for all files.") results = ( self._run_checks_parallel() if len(self._files_to_check) > 1 else self._run_checks_sync() ) self._update_results(results) def print_result(self, output_file: t.TextIO | None = None) -> int: """Print all cached error messages and return exit code. :param output_file: file to print to; defaults to sys.stderr (if ``None``) :return: exit code 0 if no error is printed; 1 if any error is printed """ if len(self.errors) == 0 and len(self._nonexisting_paths) == 0: print("Success! No issues detected.", file=output_file or sys.stdout) return 0 err_msg_regex = re.compile(r"\([A-Z]+/[0-9]+\)") for error in self.errors: err_msg = error["message"] if not err_msg_regex.match(err_msg): err_msg = "(ERROR/3) " + err_msg message = f"{error['source_origin']}:{error['line_number']}: {err_msg}" print(message, file=output_file or sys.stderr) print("Error! Issues detected.", file=output_file or sys.stderr) return 1 def run(self) -> int: # pragma: no cover """Run checks, print error messages and return the result. :return: exit code 0 if no error is printed; 1 if any error is printed """ logger.info("Run checks and print results.") self.check() return self.print_result() rstcheck-core-1.2.1/src/rstcheck_core/types.py000066400000000000000000000047131457751767200214240ustar00rootroot00000000000000"""Helper types.""" from __future__ import annotations import pathlib import typing as t SourceFileOrString = t.Union[pathlib.Path, t.Literal["", ""]] # noqa: UP007 """Path to source file or if it is a string then '' or ''.""" class LintError(t.TypedDict): """Dict with information about an linting error.""" source_origin: SourceFileOrString line_number: int message: str YieldedLintError = t.Generator[LintError, None, None] """Yielded version of type :py:class:`LintError`.""" class IgnoreDict(t.TypedDict): """Dict with ignore information.""" messages: t.Pattern[str] | None languages: list[str] directives: list[str] roles: list[str] substitutions: list[str] def construct_ignore_dict( messages: t.Pattern[str] | None = None, languages: list[str] | None = None, directives: list[str] | None = None, roles: list[str] | None = None, substitutions: list[str] | None = None, ) -> IgnoreDict: """Create an :py:class:`IgnoreDict` with passed values or defaults. :param messages: Value for :py:attr:`IgnoreDict.messages`; :py:obj:`None` results in an empty list; defaults to :py:obj:`None` :param directives: Value for :py:attr:`IgnoreDict.directives`; :py:obj:`None` results in an empty list; defaults to :py:obj:`None` :param roles: Value for :py:attr:`IgnoreDict.roles`; :py:obj:`None` results in an empty list; defaults to :py:obj:`None` :param substitutions: Value for :py:attr:`IgnoreDict.substitutions`; :py:obj:`None` results in an empty list; defaults to :py:obj:`None` :return: :py:class:`IgnoreDict` with passed values or defaults """ return IgnoreDict( messages=messages, languages=languages if languages is not None else [], directives=directives if directives is not None else [], roles=roles if roles is not None else [], substitutions=substitutions if substitutions is not None else [], ) CheckerRunFunction = t.Callable[..., YieldedLintError] """Function to run checks. Returned by :py:meth:`rstcheck_core.checker.CodeBlockChecker.create_checker`. """ class InlineConfig(t.TypedDict): """Dict with a config key and config value comming from a inline config comment.""" key: str value: str class InlineFlowControl(t.TypedDict): """Dict with a flow control value and line number comming from a inline config comment.""" value: str line_number: int rstcheck-core-1.2.1/testing/000077500000000000000000000000001457751767200157515ustar00rootroot00000000000000rstcheck-core-1.2.1/testing/examples/000077500000000000000000000000001457751767200175675ustar00rootroot00000000000000rstcheck-core-1.2.1/testing/examples/bad/000077500000000000000000000000001457751767200203155ustar00rootroot00000000000000rstcheck-core-1.2.1/testing/examples/bad/bash.rst000066400000000000000000000000461457751767200217640ustar00rootroot00000000000000==== Test ==== .. code:: bash { rstcheck-core-1.2.1/testing/examples/bad/code.rst000066400000000000000000000000551457751767200217610ustar00rootroot00000000000000==== Test ==== .. code:: python print( rstcheck-core-1.2.1/testing/examples/bad/cpp.rst000066400000000000000000000001141457751767200216250ustar00rootroot00000000000000==== Test ==== .. code:: cpp int main() { return x; } rstcheck-core-1.2.1/testing/examples/bad/markdown.rst000066400000000000000000000001001457751767200226600ustar00rootroot00000000000000==== Test ==== [Markdown-style Link](https://www.example.com/) rstcheck-core-1.2.1/testing/examples/bad/python.rst000066400000000000000000000000551457751767200223700ustar00rootroot00000000000000==== Test ==== .. code:: python print( rstcheck-core-1.2.1/testing/examples/bad/rst.rst000066400000000000000000000000161457751767200216540ustar00rootroot00000000000000==== Test === rstcheck-core-1.2.1/testing/examples/bad/rst_in_rst.rst000066400000000000000000000000631457751767200232340ustar00rootroot00000000000000==== Test ==== .. code:: rst Testing === rstcheck-core-1.2.1/testing/examples/bad/table_substitutions.rst000066400000000000000000000017661457751767200251670ustar00rootroot00000000000000Testing ======= .. |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| ==== ========== rstcheck-core-1.2.1/testing/examples/custom/000077500000000000000000000000001457751767200211015ustar00rootroot00000000000000rstcheck-core-1.2.1/testing/examples/custom/custom_directive_and_role.rst000066400000000000000000000000571457751767200270500ustar00rootroot00000000000000.. custom-directive:: :custom-role:`testing` rstcheck-core-1.2.1/testing/examples/custom/rstcheck.custom.ini000066400000000000000000000001071457751767200247170ustar00rootroot00000000000000[rstcheck] ignore_directives=custom-directive ignore_roles=custom-role rstcheck-core-1.2.1/testing/examples/good/000077500000000000000000000000001457751767200205175ustar00rootroot00000000000000rstcheck-core-1.2.1/testing/examples/good/bom.rst000066400000000000000000000003331457751767200220250ustar00rootroot00000000000000Byte order mark =============== docutils natively reports a false positive warning about the underline being to short if there is a BOM at the beginning of the title. https://github.com/rstcheck/rstcheck/issues/23 rstcheck-core-1.2.1/testing/examples/good/code_blocks.rst000066400000000000000000000033541457751767200235250ustar00rootroot00000000000000==== Test ==== .. code:: bash if [ "$x" == 'y' ] then exit 1 fi .. code:: c float foo(int n) { // Test C99. float x[n]; x[0] = 1; return x[0]; } .. code:: cpp #include int main() { auto x = 1; return x; } .. code:: python print(1) Run more tests for checking performance. .. code:: bash if [ "$x" == 'y' ] then exit 1 fi .. code:: c float foo(int n) { // Test C99. float x[n]; x[0] = 1; return x[0]; } .. code:: cpp #include int main() { auto x = 1; return x; } .. code:: python print(1) .. code:: bash if [ "$x" == 'y' ] then exit 1 fi .. code:: c float foo(int n) { // Test C99. float x[n]; x[0] = 1; return x[0]; } .. code:: cpp #include int main() { auto x = 1; return x; } .. code:: python print(1) .. code:: bash if [ "$x" == 'y' ] then exit 1 fi .. code:: c float foo(int n) { // Test C99. float x[n]; x[0] = 1; return x[0]; } .. code:: cpp #include int main() { auto x = 1; return x; } .. code:: python print(1) .. code:: bash if [ "$x" == 'y' ] then exit 1 fi .. code:: c float foo(int n) { // Test C99. float x[n]; x[0] = 1; return x[0]; } .. code:: cpp #include int main() { auto x = 1; return x; } .. code:: python # ¬∆˚ß∂ƒß∂ƒ˚¬∆ print(1) rstcheck-core-1.2.1/testing/examples/good/cpp_with_local_include.rst000066400000000000000000000001461457751767200257440ustar00rootroot00000000000000==== Test ==== .. code:: cpp #include "foo.h" int main() { return foo(); } rstcheck-core-1.2.1/testing/examples/good/foo.h000066400000000000000000000000601457751767200214470ustar00rootroot00000000000000#ifndef FOO_H #define FOO_H int foo(); #endif rstcheck-core-1.2.1/testing/examples/good/markdown.rst000066400000000000000000000001301457751767200230650ustar00rootroot00000000000000==== Test ==== .. code:: markdown [Markdown-style Link](https://www.example.com/) rstcheck-core-1.2.1/testing/examples/good/rst.rst000066400000000000000000000000171457751767200220570ustar00rootroot00000000000000==== Test ==== rstcheck-core-1.2.1/testing/examples/good/unicode.rst000066400000000000000000000002551457751767200227010ustar00rootroot00000000000000==== Тест ==== .. code:: python print("Привет!") .. code:: bash $ echo 'Привет' >> pipe.txt $ echo 'файловая труба!' >> pipe.txt rstcheck-core-1.2.1/testing/examples/inline_config/000077500000000000000000000000001457751767200223725ustar00rootroot00000000000000rstcheck-core-1.2.1/testing/examples/inline_config/with_inline_ignore.rst000066400000000000000000000006461457751767200270060ustar00rootroot00000000000000Inline 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 rstcheck-core-1.2.1/testing/examples/inline_config/with_inline_skip_code_block.rst000066400000000000000000000001541457751767200306270ustar00rootroot00000000000000.. code-block:: python print( .. rstcheck: ignore-next-code-block .. code-block:: python print( rstcheck-core-1.2.1/testing/examples/inline_config/with_nested_inline_skip_code_block.rst000066400000000000000000000002521457751767200321700ustar00rootroot00000000000000.. code-block:: rst .. code-block:: python print( .. code-block:: rst .. rstcheck: ignore-next-code-block .. code-block:: python print( rstcheck-core-1.2.1/testing/examples/inline_config/without_inline_ignore.rst000066400000000000000000000003521457751767200275300ustar00rootroot00000000000000Inline 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| rstcheck-core-1.2.1/testing/examples/sphinx/000077500000000000000000000000001457751767200211005ustar00rootroot00000000000000rstcheck-core-1.2.1/testing/examples/sphinx/good.rst000066400000000000000000000000641457751767200225620ustar00rootroot00000000000000==== Test ==== File reeference: :file:`~/.bashrc`. rstcheck-core-1.2.1/testing/examples/with_configuration/000077500000000000000000000000001457751767200234715ustar00rootroot00000000000000rstcheck-core-1.2.1/testing/examples/with_configuration/.rstcheck.cfg000066400000000000000000000002471457751767200260410ustar00rootroot00000000000000[rstcheck] ignore_directives=foobar,custom-directive ignore_roles=custom-role ignore_messages=(Title underline too short\.$) ignore_languages=cpp report_level=warning rstcheck-core-1.2.1/testing/examples/with_configuration/bad.rst000066400000000000000000000001761457751767200247550ustar00rootroot00000000000000Subtitle ------- .. custom-directive:: :custom-role:`testing` .. code:: cpp int main() { return x; } rstcheck-core-1.2.1/testing/examples/with_configuration/bad_config.cfg000066400000000000000000000001211457751767200262170ustar00rootroot00000000000000[rstcheck] ignore_directives=foobar,custom-directive report=warning unknown=true rstcheck-core-1.2.1/testing/examples/with_configuration/bad_config.toml000066400000000000000000000001451457751767200264410ustar00rootroot00000000000000[tool.rstcheck] ignore_directives = ["foobar", "custom-directive"] report = "warning" unknown = true rstcheck-core-1.2.1/testing/examples/with_configuration/bad_rst.rst000066400000000000000000000000601457751767200256350ustar00rootroot00000000000000-------- Duplicate ========= Subtitle ------- rstcheck-core-1.2.1/testing/examples/with_configuration/dummydir/000077500000000000000000000000001457751767200253235ustar00rootroot00000000000000rstcheck-core-1.2.1/testing/examples/with_configuration/dummydir/.gitkeep000066400000000000000000000000001457751767200267420ustar00rootroot00000000000000rstcheck-core-1.2.1/testing/examples/with_configuration/pyproject.toml000066400000000000000000000003311457751767200264020ustar00rootroot00000000000000[tool.rstcheck] ignore_directives = [ "foobar", "custom-directive" ] ignore_roles = [ "custom-role" ] ignore_messages = [ "(Title underline too short\\.$)" ] ignore_languages = [ "cpp" ] report_level = "warning" rstcheck-core-1.2.1/testing/examples/with_configuration/rstcheck.ini000066400000000000000000000003041457751767200257750ustar00rootroot00000000000000[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 rstcheck-core-1.2.1/testing/examples/without_configuration/000077500000000000000000000000001457751767200242215ustar00rootroot00000000000000rstcheck-core-1.2.1/testing/examples/without_configuration/bad.rst000066400000000000000000000002451457751767200255020ustar00rootroot00000000000000.. copy of with_configuration/bad.rst Subtitle ------- .. custom-directive:: :custom-role:`testing` .. code:: cpp int main() { return x; } rstcheck-core-1.2.1/tests/000077500000000000000000000000001457751767200154365ustar00rootroot00000000000000rstcheck-core-1.2.1/tests/__init__.py000066400000000000000000000000001457751767200175350ustar00rootroot00000000000000rstcheck-core-1.2.1/tests/_docutils_test.py000066400000000000000000000150741457751767200210430ustar00rootroot00000000000000"""Tests for ``_docutils`` module.""" from __future__ import annotations import docutils.parsers.rst.directives as docutils_directives import docutils.parsers.rst.roles as docutils_roles import pytest from rstcheck_core import _docutils, _extras class TestIgnoreDirectivesAndRoles: """Test ``ignore_directives_and_roles`` function.""" @staticmethod @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_with_empty_lists() -> None: """Test with empty lists.""" directives: list[str] = [] roles: list[str] = [] _docutils.ignore_directives_and_roles(directives, roles) # act assert not docutils_directives._directives # type: ignore[attr-defined] assert not docutils_roles._roles # type: ignore[attr-defined] @staticmethod @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_with_only_roles() -> None: """Test with only roles to add.""" directives: list[str] = [] roles = ["test_role"] _docutils.ignore_directives_and_roles(directives, roles) # act assert not docutils_directives._directives # type: ignore[attr-defined] assert "test_role" in docutils_roles._roles # type: ignore[attr-defined] @staticmethod @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_with_only_directives() -> None: """Test with only directives to add.""" directives = ["test_directive"] roles: list[str] = [] _docutils.ignore_directives_and_roles(directives, roles) # act assert "test_directive" in docutils_directives._directives # type: ignore[attr-defined] assert not docutils_roles._roles # type: ignore[attr-defined] @staticmethod @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_with_both() -> None: """Test with both.""" directives = ["test_directive"] roles = ["test_role"] _docutils.ignore_directives_and_roles(directives, roles) # act assert "test_directive" in docutils_directives._directives # type: ignore[attr-defined] assert "test_role" in docutils_roles._roles # type: ignore[attr-defined] class TestRegisterCodeRirective: """Test ``register_code_directive`` function.""" @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_does_nothing_when_sphinx_installed() -> None: """Test function does nothing when sphinx is installed.""" _docutils.register_code_directive() # act assert "code" not in docutils_directives._directives # type: ignore[attr-defined] assert "code-block" not in docutils_directives._directives # type: ignore[attr-defined] assert "sourcecode" not in docutils_directives._directives # type: ignore[attr-defined] @staticmethod @pytest.mark.skipif(_extras.SPHINX_INSTALLED, reason="Test without sphinx extra.") @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_registers_all_when_sphinx_is_missing() -> None: """Test function registers all directives when sphinx is missing.""" _docutils.register_code_directive() # act assert "code" in docutils_directives._directives # type: ignore[attr-defined] assert "code-block" in docutils_directives._directives # type: ignore[attr-defined] assert "sourcecode" in docutils_directives._directives # type: ignore[attr-defined] @staticmethod @pytest.mark.skipif(_extras.SPHINX_INSTALLED, reason="Test without sphinx extra.") @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_does_nothing_when_sphinx_is_missing_and_all_ignored() -> None: """Test function does nothing when sphinx is missing, but all ignores are ``True``.""" _docutils.register_code_directive( # act ignore_code_directive=True, ignore_codeblock_directive=True, ignore_sourcecode_directive=True, ) assert "code" not in docutils_directives._directives # type: ignore[attr-defined] assert "code-block" not in docutils_directives._directives # type: ignore[attr-defined] assert "sourcecode" not in docutils_directives._directives # type: ignore[attr-defined] @staticmethod @pytest.mark.skipif(_extras.SPHINX_INSTALLED, reason="Test without sphinx extra.") @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_install_only_code_when_others_are_ignored() -> None: """Test function installes only code directive when others are ignored.""" _docutils.register_code_directive( # act ignore_codeblock_directive=True, ignore_sourcecode_directive=True ) assert "code" in docutils_directives._directives # type: ignore[attr-defined] assert "code-block" not in docutils_directives._directives # type: ignore[attr-defined] assert "sourcecode" not in docutils_directives._directives # type: ignore[attr-defined] @staticmethod @pytest.mark.skipif(_extras.SPHINX_INSTALLED, reason="Test without sphinx extra.") @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_install_only_code_block_when_others_are_ignored() -> None: """Test function installes only code-block directive when others are ignored.""" _docutils.register_code_directive( # act ignore_code_directive=True, ignore_sourcecode_directive=True ) assert "code" not in docutils_directives._directives # type: ignore[attr-defined] assert "code-block" in docutils_directives._directives # type: ignore[attr-defined] assert "sourcecode" not in docutils_directives._directives # type: ignore[attr-defined] @staticmethod @pytest.mark.skipif(_extras.SPHINX_INSTALLED, reason="Test without sphinx extra.") @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_install_only_sourcecode_when_others_are_ignored() -> None: """Test function installes only sourcecode directive when others are ignored.""" _docutils.register_code_directive( # act ignore_code_directive=True, ignore_codeblock_directive=True ) assert "code" not in docutils_directives._directives # type: ignore[attr-defined] assert "code-block" not in docutils_directives._directives # type: ignore[attr-defined] assert "sourcecode" in docutils_directives._directives # type: ignore[attr-defined] rstcheck-core-1.2.1/tests/_extras_test.py000066400000000000000000000061411457751767200205160ustar00rootroot00000000000000"""Tests for ``_extras`` module.""" from __future__ import annotations import importlib.metadata import pytest from rstcheck_core import _extras class TestInstallChecker: """Test ``is_installed_with_supported_version``.""" @staticmethod @pytest.mark.skipif(_extras.SPHINX_INSTALLED, reason="Test without sphinx extra.") def test_false_on_missing_sphinx_package() -> None: """Test install-checker returns ``False`` when ``sphinx`` is missing.""" result = _extras.is_installed_with_supported_version("sphinx") assert result is False @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") def test_true_on_installed_sphinx_package() -> None: """Test install-checker returns ``True`` when ``sphinx`` is installed with good version.""" result = _extras.is_installed_with_supported_version("sphinx") assert result is True @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") def test_false_on_installed_sphinx_package_too_old(monkeypatch: pytest.MonkeyPatch) -> None: """Test install-checker returns ``False`` when ``sphinx`` is installed with bad version.""" monkeypatch.setattr(importlib.metadata, "version", lambda _: "0.0") result = _extras.is_installed_with_supported_version("sphinx") assert result is False class TestInstallGuard: """Test ``install_guard``.""" @staticmethod @pytest.mark.skipif(_extras.SPHINX_INSTALLED, reason="Test without sphinx extra.") def test_error_on_missing_sphinx_package() -> None: """Test install-guard raises exception when ``sphinx`` is missing.""" with pytest.raises(ModuleNotFoundError): _extras.install_guard("sphinx") # act @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") def test_ok_on_installed_sphinx_package() -> None: """Test install-guard doesn't raise when ``sphinx`` is installed.""" _extras.install_guard("sphinx") # act class TestTomliInstallGuard: """Test ``install_guard_tomli``.""" @staticmethod @pytest.mark.skipif(_extras.TOMLI_INSTALLED, reason="Test without tomli extra.") def test_error_tomllib_imported_is_false_and_on_missing_tomli_package() -> None: """Test raises exception when ``tomllib_imported`` is `False` and ``tomli`` is missing.""" with pytest.raises(ModuleNotFoundError): _extras.install_guard_tomli(tomllib_imported=False) # act @staticmethod @pytest.mark.skipif(not _extras.TOMLI_INSTALLED, reason="Depends on tomli extra.") def test_ok_when_tomllib_imported_is_false_and_tomli_package_is_installed() -> None: """Test doesn't raise when ``tomllib_imported`` is `False` but ``tomli`` is installed.""" _extras.install_guard_tomli(tomllib_imported=False) # act @staticmethod def test_ok_when_tomllib_imported_is_true() -> None: """Test doesn't raise when ``tomllib_imported`` is `True`.""" _extras.install_guard_tomli(tomllib_imported=True) # act rstcheck-core-1.2.1/tests/_sphinx_test.py000066400000000000000000000157651457751767200205350ustar00rootroot00000000000000"""Tests for ``_sphinx`` module.""" from __future__ import annotations import typing as t import docutils.parsers.rst.directives as docutils_directives import docutils.parsers.rst.roles as docutils_roles import pytest from rstcheck_core import _extras, _sphinx if _extras.SPHINX_INSTALLED: import sphinx.application @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") def test_dummy_app_creator() -> None: """Test creation of dummy sphinx app.""" result = _sphinx.create_dummy_sphinx_app() assert isinstance(result, sphinx.application.Sphinx) class TestContextManager: """Test ``load_sphinx_if_available`` context manager.""" @staticmethod @pytest.mark.skipif(_extras.SPHINX_INSTALLED, reason="Test without sphinx extra.") @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_yield_nothing_with_sphinx_missing() -> None: """Test for ``None`` yield and no action when sphinx is missing.""" with _sphinx.load_sphinx_if_available() as ctx_manager: assert ctx_manager is None assert not docutils_directives._directives # type: ignore[attr-defined] assert not docutils_roles._roles # type: ignore[attr-defined] @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_yield_nothing_with_sphinx_installed() -> None: """Test for ``None`` yield but action when sphinx is installed.""" with _sphinx.load_sphinx_if_available() as ctx_manager: assert ctx_manager is None assert docutils_directives._directives # type: ignore[attr-defined] assert docutils_roles._roles # type: ignore[attr-defined] assert "sphinx.addnodes" not in sphinx.application.builtin_extensions class TestSphinxDirectiveAndRoleGetter: """Test ``get_sphinx_directives_and_roles`` function.""" @staticmethod @pytest.mark.skipif(_extras.SPHINX_INSTALLED, reason="Test without sphinx extra.") def test_exception_on_missing_sphinx() -> None: """Test that the install guard triggers.""" with pytest.raises(ModuleNotFoundError): _sphinx.get_sphinx_directives_and_roles() @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_c_domain_is_loaded() -> None: """Test C domain is loaded.""" (result_directives, result_roles) = _sphinx.get_sphinx_directives_and_roles() # act assert "function" in result_directives assert "c:function" in result_directives assert "member" in result_roles assert "c:member" in result_roles @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_cpp_domain_is_loaded() -> None: """Test C++ domain is loaded.""" (result_directives, result_roles) = _sphinx.get_sphinx_directives_and_roles() # act assert "function" in result_directives assert "cpp:function" in result_directives assert "member" in result_roles assert "cpp:member" in result_roles @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_javascript_domain_is_loaded() -> None: """Test JavaScript domain is loaded.""" (result_directives, result_roles) = _sphinx.get_sphinx_directives_and_roles() # act assert "function" in result_directives assert "js:function" in result_directives assert "func" in result_roles assert "js:func" in result_roles @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") @pytest.mark.usefixtures("patch_docutils_directives_and_roles_dict") def test_python_domain_is_loaded() -> None: """Test Python domain is loaded.""" (result_directives, result_roles) = _sphinx.get_sphinx_directives_and_roles() # act assert "function" in result_directives assert "py:function" in result_directives assert "func" in result_roles assert "py:func" in result_roles @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") def test_docutils_state_dict_is_loaded(monkeypatch: pytest.MonkeyPatch) -> None: """Test docutils' state is loaded.""" test_dict_directives: dict[str, t.Any] = {"test-directive": "test-directive"} monkeypatch.setattr("sphinx.util.docutils.directives._directives", test_dict_directives) test_dict_roles: dict[str, t.Any] = {"test-role": "test-role"} monkeypatch.setattr("sphinx.util.docutils.roles._roles", test_dict_roles) (result_directives, result_roles) = _sphinx.get_sphinx_directives_and_roles() # act assert "test-directive" in result_directives assert "test-role" in result_roles class TestDirectiveAndRoleFilter: """Test ``filter_whitelisted_directives_and_roles`` function.""" @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") def test_directives_are_filtered(monkeypatch: pytest.MonkeyPatch) -> None: """Test directives are filtered.""" monkeypatch.setattr(_sphinx, "_DIRECTIVE_WHITELIST", ["test-directive"]) unfiltered_directives = ["test-directive", "test-directive2"] (result_directives, _) = _sphinx.filter_whitelisted_directives_and_roles( unfiltered_directives, [] ) # act assert "test-directive" not in result_directives assert "test-directive2" in result_directives @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") def test_code_directives_are_filtered() -> None: """Test code directives are filtered.""" (unfiltered_directives, _) = _sphinx.get_sphinx_directives_and_roles() (result_directives, _) = _sphinx.filter_whitelisted_directives_and_roles( unfiltered_directives, [] ) # act assert "code" not in result_directives assert "code-block" not in result_directives assert "sourcecode" not in result_directives @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") def test_roles_are_filtered(monkeypatch: pytest.MonkeyPatch) -> None: """Test roles are filtered.""" monkeypatch.setattr(_sphinx, "_ROLE_WHITELIST", ["test-role"]) unfiltered_roles = ["test-role", "test-role2"] (_, result_roles) = _sphinx.filter_whitelisted_directives_and_roles( [], unfiltered_roles ) # act assert "test-role" not in result_roles assert "test-role2" in result_roles rstcheck-core-1.2.1/tests/checker_test.py000066400000000000000000001334321457751767200204610ustar00rootroot00000000000000"""Tests for ``checker`` module.""" from __future__ import annotations import os import pathlib import re import shlex import sys import typing as t from inspect import isfunction import docutils.io import docutils.nodes import docutils.utils import pytest from rstcheck_core import _extras, _sphinx, checker, config, types if t.TYPE_CHECKING: import pytest_mock def test_check_file(monkeypatch: pytest.MonkeyPatch) -> None: """Test ``check_file`` returns accumulated errors from ``check_source``.""" errors = [ types.LintError(source_origin="", line_number=0, message=""), types.LintError(source_origin="", line_number=1, message=""), ] monkeypatch.setattr(checker, "_get_source", lambda _: "source") monkeypatch.setattr( checker, "check_source", lambda _, source_file, ignores, report_level, warn_unknown_settings: (e for e in errors), ) test_config = config.RstcheckConfig(config_path=pathlib.Path()) result = checker.check_file(pathlib.Path(), test_config) assert result == errors class TestRunConfigLoader: """Test ``_load_run_config`` function.""" @staticmethod def test_global_config_path_set() -> None: """Test config path is set in main config. This results in no change -> return of main config. """ test_config = config.RstcheckConfig(config_path=pathlib.Path()) result = checker._load_run_config(pathlib.Path(), test_config) assert result == test_config @staticmethod def test_no_config_file_in_dir_tree(monkeypatch: pytest.MonkeyPatch) -> None: """Test config path is unset in main config and file dir tree has no config file. This results in no change -> return of main config. """ monkeypatch.setattr(config, "load_config_file_from_dir_tree", lambda _: None) test_config = config.RstcheckConfig() result = checker._load_run_config(pathlib.Path(), test_config) assert result == test_config @staticmethod def test_config_file_in_dir_tree(monkeypatch: pytest.MonkeyPatch) -> None: """Test config path is unset in main config and file dir tree has a config file. This results in merge of configs -> return merged config. """ test_file_config = config.RstcheckConfigFile(report_level=config.ReportLevel.SEVERE) monkeypatch.setattr(config, "load_config_file_from_dir_tree", lambda _: test_file_config) test_config = config.RstcheckConfig() result = checker._load_run_config(pathlib.Path(), test_config, overwrite_config=True) assert result is not None assert test_config != config.ReportLevel.SEVERE assert result.report_level == config.ReportLevel.SEVERE class TestSourceGetter: """Test ``_get_source`` function.""" @staticmethod def test_file_name_is_dash(monkeypatch: pytest.MonkeyPatch) -> None: """Test when file name is a dash, stdin is read.""" source = "Teststring" monkeypatch.setattr(sys.stdin, "read", lambda: source) test_file = pathlib.Path("-") result = checker._get_source(test_file) assert result == source @staticmethod def test_file_name_is_not_dash(tmp_path: pathlib.Path) -> None: """Test when file name is not a dash, the file is read.""" source = """ Test ==== """ test_file = tmp_path / "testfile.rst" test_file.write_text(source) result = checker._get_source(test_file) assert result == source def test__replace_ignored_substitutions() -> None: """Test ``_replace_ignored_substitutions`` fucntion replaces substitutions.""" source = "|Substitution1| |Substitution2|" result = checker._replace_ignored_substitutions(source, ["Substitution1"]) assert result == "xSubstitution1x |Substitution2|" def test__create_ignore_dict_from_config() -> None: """Test ``_create_ignore_dict_from_config`` fucntion creates ignore dict.""" ignore_messages = r"foo/bar" ignore_messages_re = re.compile(ignore_messages) ignore_languages = ["python", "cpp"] ignore_directives = ["code"] ignore_roles = ["role"] ignore_substitutions = ["substi"] test_config = config.RstcheckConfig( ignore_messages=ignore_messages, ignore_languages=ignore_languages, ignore_directives=ignore_directives, ignore_roles=ignore_roles, ignore_substitutions=ignore_substitutions, ) result = checker._create_ignore_dict_from_config( test_config, ) assert result == types.construct_ignore_dict( messages=ignore_messages_re, languages=ignore_languages, directives=ignore_directives, roles=ignore_roles, substitutions=ignore_substitutions, ) class TestSourceChecker: """Test ``check_source`` fucntion.""" @staticmethod def test_empty_source() -> None: """Test empty source generates no errors.""" source = "" result = list(checker.check_source(source)) assert not result @staticmethod def test_lint_error_no_source_file() -> None: """Test lint error holds "" if no source file is passed.""" source = """ Test === """ result = list(checker.check_source(source)) assert len(result) == 1 assert result[0]["source_origin"] == "" @staticmethod def test_lint_error_with_source_file() -> None: """Test lint error holds source file.""" test_file = pathlib.Path("test_file.rst") source = """ Test === """ result = list(checker.check_source(source, test_file)) assert len(result) == 1 assert result[0]["source_origin"] == test_file @staticmethod def test_lint_error_returned_on_default_ignore() -> None: """Test lint error is returned with default ignores.""" source = """ Test === """ result = list(checker.check_source(source)) assert len(result) == 1 assert "Possible title underline, too short for the title" in result[0]["message"] @staticmethod def test_lint_error_skipped_on_set_ignore() -> None: """Test lint error is skipped with set ignores.""" source = """ Test === """ ignores = types.construct_ignore_dict( messages=re.compile(r"Possible title underline, too short for the title") ) result = list(checker.check_source(source, ignores=ignores)) assert not result @staticmethod @pytest.mark.skipif(_extras.SPHINX_INSTALLED, reason="Test without sphinx extra.") def test_sphinx_directive_errors_without_sphinx() -> None: """Test error on sphinx directive when sphinx is missing.""" source = """ .. py:function:: foo """ result = list(checker.check_source(source)) assert len(result) > 0 assert 'No directive entry for "py:function"' in result[0]["message"] @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") def test_sphinx_directive_does_not_error_with_sphinx() -> None: """Test no error on sphinx directive when sphinx is installed.""" source = """ .. py:function:: foo """ result = list(checker.check_source(source)) assert not result @staticmethod @pytest.mark.skipif(sys.version_info[0:2] > (3, 9), reason="Requires python3.9 or lower") def test_code_block_lint_error_returned_on_default_ignore_pre310() -> None: """Test code lint error is returned with default ignores. In Python version 3.10 the error messag changed. """ source = """ .. code:: python print( """ result = list(checker.check_source(source)) assert len(result) == 1 assert "unexpected EOF while parsing" in result[0]["message"] @staticmethod @pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires python3.10 or higher") def test_code_block_lint_error_returned_on_default_ignore() -> None: """Test code lint error is returned with default ignores.""" source = """ .. code:: python print( """ result = list(checker.check_source(source)) assert len(result) == 1 assert "'(' was never closed" in result[0]["message"] @staticmethod @pytest.mark.skipif(sys.version_info[0:2] > (3, 9), reason="Requires python3.9 or lower") def test_code_block_no_error_on_set_ignore_pre310() -> None: """Test code lint error is skipped with set ignores. In Python version 3.10 the error messag changed. """ source = """ .. code:: python print( """ ignores = types.construct_ignore_dict(messages=re.compile(r"unexpected EOF while parsing")) result = list(checker.check_source(source, ignores=ignores)) assert not result @staticmethod @pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires python3.10 or higher") def test_code_block_no_error_on_set_ignore() -> None: """Test code lint error is skipped with set ignores.""" source = """ .. code:: python print( """ ignores = types.construct_ignore_dict(messages=re.compile(r"'\(' was never closed")) result = list(checker.check_source(source, ignores=ignores)) assert not result @staticmethod def test_stdin_message() -> None: """Test code lint error message for stdin.""" source = """ .. code:: python print( """ ignores = types.construct_ignore_dict() result = list(checker.check_source(source, source_file=pathlib.Path("-"), ignores=ignores)) assert len(result) == 1 assert result[0]["source_origin"] == "" @staticmethod @pytest.mark.skipif(_extras.SPHINX_INSTALLED, reason="Test without sphinx extra.") @pytest.mark.skipif(sys.version_info[0:2] > (3, 9), reason="Requires python3.9 or lower") @pytest.mark.parametrize("code_block_directive", ["code", "code-block", "sourcecode"]) def test_code_block_without_language_works_without_sphinx_pre310( code_block_directive: str, ) -> None: """Test code blocks without a language are not checked and do not error without sphinx.""" source = f""" .. {code_block_directive}:: print( .. {code_block_directive}:: python print( """ ignores = types.construct_ignore_dict() result = list(checker.check_source(source, ignores=ignores)) assert len(result) == 1 assert result[0]["line_number"] == 8 assert "unexpected EOF while parsing" in result[0]["message"] @staticmethod @pytest.mark.skipif(_extras.SPHINX_INSTALLED, reason="Test without sphinx extra.") @pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires python3.10 or higher") @pytest.mark.parametrize("code_block_directive", ["code", "code-block", "sourcecode"]) def test_code_block_without_language_works_without_sphinx( code_block_directive: str, ) -> None: """Test code blocks without a language are not checked and do not error without sphinx.""" source = f""" .. {code_block_directive}:: print( .. {code_block_directive}:: python print( """ ignores = types.construct_ignore_dict() result = list(checker.check_source(source, ignores=ignores)) assert len(result) == 1 assert result[0]["line_number"] == 8 assert "'(' was never closed" in result[0]["message"] @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") @pytest.mark.skipif(sys.version_info[0:2] > (3, 9), reason="Requires python3.9 or lower") @pytest.mark.xfail( reason="Sphinx support fails for language-less code blocks. See #3", strict=True ) @pytest.mark.parametrize("code_block_directive", ["code", "code-block", "sourcecode"]) def test_code_block_without_language_is_works_with_sphinx_pre310( code_block_directive: str, ) -> None: """Test code blocks without a language are working and do not error with sphinx.""" source = f""" .. {code_block_directive}:: print( .. {code_block_directive}:: python print( """ ignores = types.construct_ignore_dict() # fmt: off with _sphinx.load_sphinx_if_available(): result = list(checker.check_source(source, ignores=ignores)) # fmt: on assert len(result) == 1 assert result[0]["line_number"] == 9 assert "unexpected EOF while parsing" in result[0]["message"] @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") @pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires python3.10 or higher") @pytest.mark.xfail( reason="Sphinx support fails for language-less code blocks. See #3", strict=True ) @pytest.mark.parametrize("code_block_directive", ["code", "code-block", "sourcecode"]) def test_code_block_without_language_is_works_with_sphinx( code_block_directive: str, ) -> None: """Test code blocks without a language are working and do not error with sphinx.""" source = f""" .. {code_block_directive}:: print( .. {code_block_directive}:: python print( """ ignores = types.construct_ignore_dict() # fmt: off with _sphinx.load_sphinx_if_available(): result = list(checker.check_source(source, ignores=ignores)) # fmt: on assert len(result) == 1 assert result[0]["line_number"] == 9 assert "'(' was never closed" in result[0]["message"] @staticmethod @pytest.mark.skipif(_extras.SPHINX_INSTALLED, reason="Test without sphinx extra.") @pytest.mark.parametrize("code_block_directive", ["code", "code-block", "sourcecode"]) def test_code_block_without_language_logs_nothing_without_sphinx( code_block_directive: str, caplog: pytest.LogCaptureFixture, ) -> None: """Test code blocks without a language log nothing and do not error without sphinx. Conter part to the XFAIL tests ``test_code_block_without_language_is_works_with_sphinx``. """ source = f""" .. {code_block_directive}:: print( .. {code_block_directive}:: python print( """ ignores = types.construct_ignore_dict() # fmt: off with _sphinx.load_sphinx_if_available(): result = list(checker.check_source(source, ignores=ignores)) # fmt: on assert result assert "An `AttributeError` error occured" not in caplog.text assert ( "directive (code/code-block/sourcecode) without a specified language" not in caplog.text ) @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") @pytest.mark.parametrize("code_block_directive", ["code", "code-block", "sourcecode"]) def test_code_block_without_language_logs_critcal_with_sphinx( code_block_directive: str, caplog: pytest.LogCaptureFixture, ) -> None: """Test code blocks without a language log critical and do not error with sphinx. Conter part to the XFAIL tests ``test_code_block_without_language_is_works_with_sphinx``. """ source = f""" .. {code_block_directive}:: print( .. {code_block_directive}:: python print( """ ignores = types.construct_ignore_dict() # fmt: off with _sphinx.load_sphinx_if_available(): result = list(checker.check_source(source, ignores=ignores)) # fmt: on assert not result assert "An `AttributeError` error occured" in caplog.text assert "directive (code/code-block/sourcecode) without a specified language" in caplog.text class TestCodeCheckRunner: """Test ``_run_code_checker_and_filter_errors`` function.""" @staticmethod def test_without_ignore() -> None: """Test both checkers return error without ignore.""" cb_checker = checker.CodeBlockChecker("") checker_list = [ cb_checker.create_checker("print(", "python"), cb_checker.create_checker("{", "json"), ] result = list(checker._run_code_checker_and_filter_errors(checker_list, None)) assert len(result) == 2 @staticmethod def test_with_ignore() -> None: """Test only one checker return error with ignore.""" cb_checker = checker.CodeBlockChecker("") checker_list = [ cb_checker.create_checker("print(", "python"), cb_checker.create_checker("{", "json"), ] ignore_messages = re.compile(r"Expecting property name enclosed in double quotes") result = list(checker._run_code_checker_and_filter_errors(checker_list, ignore_messages)) assert len(result) == 1 class TestRstErrorParseFilter: """Test ``_parse_and_filter_rst_errors`` function.""" @staticmethod def test_without_ignore() -> None: """Test both error messages are parsed and returned.""" error_str = ":1:1: Error message 1\n:1:2: Error message 2" result = list(checker._parse_and_filter_rst_errors(error_str, "", None)) assert len(result) == 2 @staticmethod def test_with_ignore() -> None: """Test only one error message is parsed and returned.""" error_str = ":1:1: Error message 1\n:1:2: Error message 2" ignore_messages = re.compile(r"Error message 1") result = list(checker._parse_and_filter_rst_errors(error_str, "", ignore_messages)) assert len(result) == 1 class TestCheckTranslator: """Test ``_CheckTranslator`` class.""" @staticmethod def test_no_checkers_on_init() -> None: """Test checkers are empty on init.""" doc = docutils.utils.new_document("") result = checker._CheckTranslator(doc, "", "") assert not result.checkers class TestCodeBlockChecker: """Test ``CodeBlockChecker`` class.""" @staticmethod def test_init() -> None: """Test nothing special happens on ``__init__`` method.""" source_origin: types.SourceFileOrString = "" result = checker.CodeBlockChecker(source_origin) assert result.source_origin == source_origin assert result.ignores is None assert result.report_level == config.ReportLevel.INFO @staticmethod def test_language_is_supported_on_supported_lang() -> None: """Test ``language_is_supported`` method returns ``True`` for supported language.""" cb_checker = checker.CodeBlockChecker("") result = cb_checker.language_is_supported("python") assert result is True @staticmethod def test_language_is_supported_on_unsupported_lang() -> None: """Test ``language_is_supported`` method returns ``False`` for unsupported language.""" cb_checker = checker.CodeBlockChecker("") result = cb_checker.language_is_supported("some-unsupported-lang") assert result is False @staticmethod def test_create_checker_returns_function() -> None: """Test ``create_checker`` method returns a lambda function. Currently unknown how to test if lambda body is ``self.check``. """ cb_checker = checker.CodeBlockChecker("") result = cb_checker.create_checker("", "") assert isfunction(result) assert result.__name__ == "" @staticmethod def test_check_returns_none_on_unsupported_lang() -> None: """Test ``check`` returns ``None`` on unsupported language.""" cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check("", "some-unsupported-lang")) assert not result @staticmethod def test_check_returns_none_on_ok_code_block_for_supported_lang() -> None: """Test ``check`` returns ``None`` on ok code block for supported language.""" source = """ print("rstcheck") """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check(source, "python")) assert not result @staticmethod @pytest.mark.skipif(sys.version_info[0:2] > (3, 9), reason="Requires python3.9 or lower") def test_check_returns_error_on_bad_code_block_for_supported_lang_pre310() -> None: """Test ``check`` returns error on bad code block for supported language. In Python version 3.10 the error messag changed. """ source = """ print( """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check(source, "python")) assert len(result) == 1 assert "unexpected EOF while parsing" in result[0]["message"] @staticmethod @pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires python3.10 or higher") def test_check_returns_error_on_bad_code_block_for_supported_lang() -> None: """Test ``check`` returns error on bad code block for supported language.""" source = """ print( """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check(source, "python")) assert len(result) == 1 assert "'(' was never closed" in result[0]["message"] @staticmethod def test_check_python_returns_none_on_ok_code_block() -> None: """Test ``check_python`` returns ``None`` on ok code block.""" source = """ print("rstcheck") """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_python(source)) assert not result @staticmethod @pytest.mark.skipif(sys.version_info[0:2] > (3, 9), reason="Requires python3.9 or lower") def test_check_python_returns_error_on_bad_code_block_pre310() -> None: """Test ``check_python`` returns error on bad code block. In Python version 3.10 the error messag changed. """ source = """ print( """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_python(source)) assert len(result) == 1 assert "unexpected EOF while parsing" in result[0]["message"] @staticmethod @pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires python3.10 or higher") def test_check_python_returns_error_on_bad_code_block() -> None: """Test ``check_python`` returns error on bad code block.""" source = """ print( """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_python(source)) assert len(result) == 1 assert "'(' was never closed" in result[0]["message"] @staticmethod @pytest.mark.skipif(sys.version_info[0:2] > (3, 11), reason="Requires python3.11 or lower") def test_check_python_returns_no_error_on_syntax_warning_pre312() -> None: """Test ``check_python`` returns no error on SyntaxWarning. With python 3.8 a SyntaxWarning is logged for '"is" with literals'. Context: https://github.com/rstcheck/rstcheck/issues/122 """ source = """ if mystring is "ok": ... """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_python(source)) assert len(result) == 1 assert '"is" with a literal' in result[0]["message"] @staticmethod @pytest.mark.skipif(sys.version_info < (3, 12), reason="Requires python3.12 or higher") def test_check_python_returns_error_on_syntax_warning() -> None: """Test ``check_python`` returns error on SyntaxWarning. With python 3.8 a SyntaxWarning is logged for '"is" with literals'. Context: https://github.com/rstcheck/rstcheck/issues/122 """ source = """ if mystring is "ok": ... """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_python(source)) assert len(result) == 1 assert "\"is\" with 'str' literal" in result[0]["message"] @staticmethod def test_check_json_returns_none_on_ok_code_block() -> None: """Test ``check_json`` returns ``None`` on ok code block.""" source = """ { "key": "value" } """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_json(source)) assert not result @staticmethod def test_check_json_returns_error_on_bad_code_block() -> None: """Test ``check_json`` returns error on bad code block.""" source = """ { "key": } """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_json(source)) assert len(result) == 1 assert "Expecting value:" in result[0]["message"] @staticmethod @pytest.mark.skipif(checker.yaml_imported is False, reason="Requires pyyaml to be installed") def test_check_yaml_returns_none_on_ok_code_block_no_pyyaml( mocker: pytest_mock.MockerFixture, ) -> None: """Test ``check_json`` returns ``None`` on ok code block.""" source = """ spam: ham eggs: ham """ mocker.patch.object(checker, "yaml_imported", False) # noqa: FBT003 cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_yaml(source)) assert not result @staticmethod @pytest.mark.skipif(checker.yaml_imported is False, reason="Requires pyyaml to be installed") def test_check_yaml_returns_ok_on_bad_code_block_no_pyyaml( mocker: pytest_mock.MockerFixture, ) -> None: """Test ``check_json`` returns error on bad code block.""" source = """ spam: ham eggs: ham """ mocker.patch.object(checker, "yaml_imported", False) # noqa: FBT003 cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_yaml(source)) assert not result @staticmethod @pytest.mark.skipif(checker.yaml_imported is False, reason="Requires pyyaml to be installed") def test_check_yaml_returns_none_on_ok_code_block() -> None: """Test ``check_json`` returns ``None`` on ok code block.""" source = """ spam: ham eggs: ham """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_yaml(source)) assert not result @staticmethod @pytest.mark.skipif(checker.yaml_imported is False, reason="Requires pyyaml to be installed") def test_check_yaml_returns_error_on_bad_code_block() -> None: """Test ``check_json`` returns error on bad code block.""" source = """ spam: ham eggs: ham """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_yaml(source)) assert len(result) == 1 assert "mapping values are not allowed here" in result[0]["message"] @staticmethod def test_check_xml_returns_none_on_ok_code_block() -> None: """Test ``check_xml`` returns ``None`` on ok code block.""" source = """ Reminder """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_xml(source)) assert not result @staticmethod def test_check_xml_returns_error_on_bad_code_block() -> None: """Test ``check_xml`` returns error on bad code block.""" source = """ Reminder """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_xml(source)) assert len(result) == 1 assert "mismatched tag:" in result[0]["message"] @staticmethod def test_check_rst_returns_none_on_ok_code_block() -> None: """Test ``check_rst`` returns ``None`` on ok code block.""" source = """ Heading ======= """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_rst(source)) assert not result @staticmethod def test_check_rst_returns_error_on_bad_code_block() -> None: """Test ``check_rst`` returns error on bad code block.""" source = """ Heading ====== """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_rst(source)) assert len(result) == 1 assert "Title underline too short." in result[0]["message"] @staticmethod def test_check_doctest_returns_none_on_ok_code_block() -> None: """Test ``check_doctest`` returns ``None`` on ok code block.""" source = """ >>> x = 1 >>> x 1 """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_doctest(source)) assert not result @staticmethod def test_check_doctest_returns_error_on_bad_code_block() -> None: """Test ``check_doctest`` returns error on bad code block.""" source = """ >>> x = 1 >>>> x 1 """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_doctest(source)) assert len(result) == 1 assert "lacks blank after >>>: '>>>> x'" in result[0]["message"] @staticmethod def test_check_doctest_returns_none_on_bad_code_block_if_regex_no_matching( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test ``check_doctest`` returns ``None`` on bad code block if regex is not matching.""" monkeypatch.setattr(checker, "DOCTEST_LINE_NO_REGEX", re.compile("bad-regex")) source = """ >>> x = 1 >>>> x 1 """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_doctest(source)) assert not result @staticmethod def test_check_bash_returns_none_on_ok_code_block() -> None: """Test ``check_bash`` returns ``None`` on ok code block.""" source = """ if [ "$x" == 'y' ] then exit 1 fi """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_bash(source)) assert not result @staticmethod @pytest.mark.skipif( sys.platform == "win32", reason="Unknown Windows specific wrong positiv. `list index out of range`", ) def test_check_bash_returns_error_on_bad_code_block() -> None: """Test ``check_bash`` returns error on bad code block.""" source = """ { """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_bash(source)) assert len(result) == 1 assert "syntax error: unexpected end of file" in result[0]["message"] @staticmethod def test_check_c_returns_none_on_ok_code_block() -> None: """Test ``check_c`` returns ``None`` on ok code block.""" source = """ float foo(int n) { // Test C99. float x[n]; x[0] = 1; return x[0]; } """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_c(source)) assert not result @staticmethod @pytest.mark.skipif(sys.platform != "linux", reason="Linux specific error message") def test_check_c_returns_error_on_bad_code_block_linux() -> None: """Test ``check_c`` returns error on bad code block.""" source = """ int main() { return x; } """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_c(source)) assert len(result) > 0 assert ( "error: \u2018x\u2019 undeclared (first use in this function)" in result[0]["message"] ) @staticmethod @pytest.mark.skipif(sys.platform != "darwin", reason="MacOS specific error message") def test_check_c_returns_error_on_bad_code_block_macos() -> None: """Test ``check_c`` returns error on bad code block.""" source = """ int main() { return x; } """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_c(source)) assert len(result) > 0 assert "error: use of undeclared identifier 'x'" in result[0]["message"] @staticmethod @pytest.mark.skipif(sys.platform != "win32", reason="Windows specific error message") def test_check_c_returns_error_on_bad_code_block_windows() -> None: """Test ``check_c`` returns error on bad code block.""" source = """ int main() { return x; } """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_c(source)) assert len(result) > 0 assert "error: 'x' undeclared (first use in this function)" in result[0]["message"] @staticmethod def test_check_cpp_returns_none_on_ok_code_block() -> None: """Test ``check_cpp`` returns ``None`` on ok code block.""" source = """ #include int main() { auto x = 1; return x; } """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_cpp(source)) assert not result @staticmethod @pytest.mark.skipif(sys.platform != "linux", reason="Linux specific error message") def test_check_cpp_returns_error_on_bad_code_block_linux() -> None: """Test ``check_cpp`` returns error on bad code block.""" source = """ int main() { return x; } """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_cpp(source)) assert len(result) == 1 assert "error: \u2018x\u2019 was not declared in this scope" in result[0]["message"] @staticmethod @pytest.mark.skipif(sys.platform != "darwin", reason="MacOS specific error message") def test_check_cpp_returns_error_on_bad_code_block_macos() -> None: """Test ``check_cpp`` returns error on bad code block.""" source = """ int main() { return x; } """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_cpp(source)) assert len(result) == 1 assert "error: use of undeclared identifier 'x'" in result[0]["message"] @staticmethod @pytest.mark.skipif(sys.platform != "win32", reason="Windows specific error message") def test_check_cpp_returns_error_on_bad_code_block_windows() -> None: """Test ``check_cpp`` returns error on bad code block.""" source = """ int main() { return x; } """ cb_checker = checker.CodeBlockChecker("") result = list(cb_checker.check_cpp(source)) assert len(result) == 1 assert "error: 'x' was not declared in this scope" in result[0]["message"] @staticmethod def test__gcc_checker_returns_none_on_ok_cpp_code_block() -> None: """Test ``_gcc_checker`` returns ``None`` on ok c++ code block.""" source = """ #include int main() { auto x = 1; return x; } """ cb_checker = checker.CodeBlockChecker("") result = list( cb_checker._gcc_checker( source, ".cpp", [ os.getenv("CXX", "g++"), *shlex.split(os.getenv("CXXFLAGS", "")), *shlex.split(os.getenv("CPPFLAGS", "")), "-I.", "-I..", "-pedantic", "-fsyntax-only", ], ) ) assert not result @staticmethod @pytest.mark.skipif(sys.platform != "linux", reason="Linux specific error message") def test__gcc_checker_returns_error_on_bad_cpp_code_block_linux() -> None: """Test ``_gcc_checker`` returns error on bad c++ code block.""" source = """ int main() { return x; } """ cb_checker = checker.CodeBlockChecker("") result = list( cb_checker._gcc_checker( source, ".cpp", [ os.getenv("CXX", "g++"), *shlex.split(os.getenv("CXXFLAGS", "")), *shlex.split(os.getenv("CPPFLAGS", "")), "-I.", "-I..", "-pedantic", "-fsyntax-only", ], ) ) assert len(result) == 1 assert "error: \u2018x\u2019 was not declared in this scope" in result[0]["message"] @staticmethod @pytest.mark.skipif(sys.platform != "darwin", reason="MacOS specific error message") def test__gcc_checker_returns_error_on_bad_cpp_code_block_macos() -> None: """Test ``_gcc_checker`` returns error on bad c++ code block.""" source = """ int main() { return x; } """ cb_checker = checker.CodeBlockChecker("") result = list( cb_checker._gcc_checker( source, ".cpp", [ os.getenv("CXX", "g++"), *shlex.split(os.getenv("CXXFLAGS", "")), *shlex.split(os.getenv("CPPFLAGS", "")), "-I.", "-I..", "-pedantic", "-fsyntax-only", ], ) ) assert len(result) == 1 assert "error: use of undeclared identifier 'x'" in result[0]["message"] @staticmethod @pytest.mark.skipif(sys.platform != "win32", reason="Windows specific error message") def test__gcc_checker_returns_error_on_bad_cpp_code_block_windows() -> None: """Test ``_gcc_checker`` returns error on bad c++ code block.""" source = """ int main() { return x; } """ cb_checker = checker.CodeBlockChecker("") result = list( cb_checker._gcc_checker( source, ".cpp", [ os.getenv("CXX", "g++"), *shlex.split(os.getenv("CXXFLAGS", "")), *shlex.split(os.getenv("CPPFLAGS", "")), "-I.", "-I..", "-pedantic", "-fsyntax-only", ], ) ) assert len(result) == 1 assert "error: 'x' was not declared in this scope" in result[0]["message"] @staticmethod def test__run_in_subprocess_returns_none_on_ok_cpp_code_block() -> None: """Test ``_run_in_subprocess`` returns ``None`` on ok c++ code block.""" source = """ #include int main() { auto x = 1; return x; } """ cb_checker = checker.CodeBlockChecker("") result = cb_checker._run_in_subprocess( source, ".cpp", [ os.getenv("CXX", "g++"), *shlex.split(os.getenv("CXXFLAGS", "")), *shlex.split(os.getenv("CPPFLAGS", "")), "-I.", "-I..", "-pedantic", "-fsyntax-only", ], ) assert not result @staticmethod @pytest.mark.skipif(sys.platform != "linux", reason="Linux specific error message") def test__run_in_subprocess_returns_error_on_bad_cpp_code_block_linux() -> None: """Test ``_run_in_subprocess`` returns error on bad c++ code block.""" source = """ int main() { return x; } """ cb_checker = checker.CodeBlockChecker("") result = cb_checker._run_in_subprocess( source, ".cpp", [ os.getenv("CXX", "g++"), *shlex.split(os.getenv("CXXFLAGS", "")), *shlex.split(os.getenv("CPPFLAGS", "")), "-I.", "-I..", "-pedantic", "-fsyntax-only", ], ) assert result is not None assert "error: \u2018x\u2019 was not declared in this scope" in result[0] assert result[1].suffix == ".cpp" @staticmethod @pytest.mark.skipif(sys.platform != "darwin", reason="MacOS specific error message") def test__run_in_subprocess_returns_error_on_bad_cpp_code_block_macos() -> None: """Test ``_run_in_subprocess`` returns error on bad c++ code block.""" source = """ int main() { return x; } """ cb_checker = checker.CodeBlockChecker("") result = cb_checker._run_in_subprocess( source, ".cpp", [ os.getenv("CXX", "g++"), *shlex.split(os.getenv("CXXFLAGS", "")), *shlex.split(os.getenv("CPPFLAGS", "")), "-I.", "-I..", "-pedantic", "-fsyntax-only", ], ) assert result is not None assert "error: use of undeclared identifier 'x'" in result[0] assert result[1].suffix == ".cpp" @staticmethod @pytest.mark.skipif(sys.platform != "win32", reason="Windows specific error message") def test__run_in_subprocess_returns_error_on_bad_cpp_code_block_windows() -> None: """Test ``_run_in_subprocess`` returns error on bad c++ code block.""" source = """ int main() { return x; } """ cb_checker = checker.CodeBlockChecker("") result = cb_checker._run_in_subprocess( source, ".cpp", [ os.getenv("CXX", "g++"), *shlex.split(os.getenv("CXXFLAGS", "")), *shlex.split(os.getenv("CPPFLAGS", "")), "-I.", "-I..", "-pedantic", "-fsyntax-only", ], ) assert result is not None assert "error: 'x' was not declared in this scope" in result[0] assert result[1].suffix == ".cpp" @staticmethod @pytest.mark.skipif(sys.platform != "linux", reason="Linux specific error message") def test__run_in_subprocess_returns_error_on_bad_cpp_code_block_with_filename_linux() -> None: """Test ``_run_in_subprocess`` returns error on bad c++ code block from filename.""" source = """ int main() { return x; } """ cb_checker = checker.CodeBlockChecker(pathlib.Path("filename.cpp")) result = cb_checker._run_in_subprocess( source, ".cpp", [ os.getenv("CXX", "g++"), *shlex.split(os.getenv("CXXFLAGS", "")), *shlex.split(os.getenv("CPPFLAGS", "")), "-I.", "-I..", "-pedantic", "-fsyntax-only", ], ) assert result is not None assert "error: \u2018x\u2019 was not declared in this scope" in result[0] assert result[1].suffix == ".cpp" @staticmethod @pytest.mark.skipif(sys.platform != "darwin", reason="MacOS specific error message") def test__run_in_subprocess_returns_error_on_bad_cpp_code_block_with_filename_macos() -> None: """Test ``_run_in_subprocess`` returns error on bad c++ code block from filename.""" source = """ int main() { return x; } """ cb_checker = checker.CodeBlockChecker(pathlib.Path("filename.cpp")) result = cb_checker._run_in_subprocess( source, ".cpp", [ os.getenv("CXX", "g++"), *shlex.split(os.getenv("CXXFLAGS", "")), *shlex.split(os.getenv("CPPFLAGS", "")), "-I.", "-I..", "-pedantic", "-fsyntax-only", ], ) assert result is not None assert "error: use of undeclared identifier 'x'" in result[0] assert result[1].suffix == ".cpp" @staticmethod @pytest.mark.skipif(sys.platform != "win32", reason="Windows specific error message") def test__run_in_subprocess_returns_error_on_bad_cpp_code_block_with_filename_windows() -> None: """Test ``_run_in_subprocess`` returns error on bad c++ code block from filename.""" source = """ int main() { return x; } """ cb_checker = checker.CodeBlockChecker(pathlib.Path("filename.cpp")) result = cb_checker._run_in_subprocess( source, ".cpp", [ os.getenv("CXX", "g++"), *shlex.split(os.getenv("CXXFLAGS", "")), *shlex.split(os.getenv("CPPFLAGS", "")), "-I.", "-I..", "-pedantic", "-fsyntax-only", ], ) assert result is not None assert "error: 'x' was not declared in this scope" in result[0] assert result[1].suffix == ".cpp" @staticmethod def test__parse_gcc_style_error_message_raises_on_bad_format() -> None: """Test ``_parse_gcc_style_error_message`` method raises ``ValueError`` on bad format.""" message = "Foo bar" with pytest.raises(ValueError, match="^Message cannot be parsed.$"): checker._parse_gcc_style_error_message(message, "") @staticmethod def test__parse_gcc_style_error_message_with_tempfile() -> None: """Test ``_parse_gcc_style_error_message`` method with tempfile.""" message = "tempfile.cpp:16:32: Error message" error = types.LintError( source_origin=pathlib.Path("source.rst"), line_number=16, message="Error message", ) result = checker._parse_gcc_style_error_message( message, pathlib.Path("source.rst"), temp_file_name=pathlib.Path("tempfile.cpp") ) assert result == error @staticmethod def test__parse_gcc_style_error_message_with_column() -> None: """Test ``_parse_gcc_style_error_message`` method with column.""" message = ":16:32: Error message" error = types.LintError( source_origin="", line_number=16, message="Error message", ) result = checker._parse_gcc_style_error_message(message, "") assert result == error @staticmethod def test__parse_gcc_style_error_message_without_column() -> None: """Test ``_parse_gcc_style_error_message`` method without column.""" message = ":16: Error message" error = types.LintError( source_origin="", line_number=16, message="Error message", ) result = checker._parse_gcc_style_error_message(message, "", has_column=False) assert result == error rstcheck-core-1.2.1/tests/config_test.py000066400000000000000000001352771457751767200203330ustar00rootroot00000000000000"""Tests for ``config`` module.""" from __future__ import annotations import logging import pathlib import re import typing as t import pytest from rstcheck_core import _extras, config def test_report_level_map_matches_numbers() -> None: """Test that the enum's values match the map's ones.""" enum_values = [e.value for e in config.ReportLevel] map_values = list(config.ReportLevelMap.values()) assert map_values == enum_values def test_report_level_map_matches_names() -> None: """Test that the enum's name match the map's keys.""" enum_names = [e.casefold() for e in config.ReportLevel._member_names_] map_keys = list(config.ReportLevelMap.keys()) assert enum_names == map_keys def test_default_values_for_config_file() -> None: """Test default values of ``RstcheckConfigFile``.""" result = config.RstcheckConfigFile() assert result.report_level is None assert result.ignore_directives is None assert result.ignore_roles is None assert result.ignore_substitutions is None assert result.ignore_languages is None assert result.ignore_messages is None def test_default_values_for_config() -> None: """Test default values of ``RstcheckConfig``.""" result = config.RstcheckConfig() assert result.report_level is None assert result.ignore_directives is None # type: ignore[unreachable] assert result.ignore_roles is None assert result.ignore_substitutions is None assert result.ignore_languages is None assert result.ignore_messages is None assert result.config_path is None assert result.recursive is None class TestSplitStrValidator: """Test ``_split_str_validator`` validator function.""" @staticmethod def test_none_means_default() -> None: """Test ``None`` results in ``None``.""" result = config._split_str_validator(None) assert result is None @staticmethod @pytest.mark.parametrize( ("string", "split_list"), [ ("value1", ["value1"]), ("value1,value2", ["value1", "value2"]), ("value1, value2", ["value1", "value2"]), ("value1 ,value2", ["value1", "value2"]), ("value1 , value2", ["value1", "value2"]), ("value1 ,\n value2", ["value1", "value2"]), ("value1 ,\n value2\n", ["value1", "value2"]), ("value1 , value2,", ["value1", "value2"]), ("value1 , value2 ,", ["value1", "value2"]), ("value1 , value2 , ", ["value1", "value2"]), ], ) def test_strings_are_transformed_to_lists(string: str, split_list: list[str]) -> None: """Test strings are split at the ",", trailing commas are ignored and whitespace cleaned.""" result = config._split_str_validator(string) assert result == split_list @staticmethod @pytest.mark.parametrize( ("string_list", "string_list_cleaned"), [ (["value1"], ["value1"]), (["value1", "value2"], ["value1", "value2"]), (["value1", " value2"], ["value1", "value2"]), (["value1 ", "value2"], ["value1", "value2"]), (["value1 ", " value2"], ["value1", "value2"]), ], ) def test_string_lists_are_whitespace_cleaned( string_list: list[str], string_list_cleaned: list[str] ) -> None: """Test lists of strings are whitespace cleaned.""" result = config._split_str_validator(string_list) assert result == string_list_cleaned @staticmethod @pytest.mark.parametrize( "value", [ 1, [1], 1.1, [1.1], False, [False], True, [True], ["foo", 1], ], ) def test_invalid_settings(value: str) -> None: """Test invalid settings.""" with pytest.raises(TypeError, match="^Not a string or list of strings$"): config._split_str_validator(value) class TestReportLevelValidatorMethod: """Test ``valid_report_level`` validator method of the ``RstcheckConfig`` class. It validates the ``report_level`` setting. """ @staticmethod def test_none_means_none() -> None: """Test ``None`` results in no report level.""" result = config.RstcheckConfigFile(report_level=None) assert result is not None assert result.report_level is None @staticmethod def test_set_level_stays() -> None: """Test set level results in same level.""" result = config.RstcheckConfigFile(report_level=config.ReportLevel.SEVERE) assert result is not None assert result.report_level is config.ReportLevel.SEVERE @staticmethod def test_empty_string_means_default() -> None: """Test empty string results in default report level.""" result = config.RstcheckConfigFile(report_level="") assert result is not None assert result.report_level is config.ReportLevel.INFO @staticmethod @pytest.mark.parametrize( "level", [1, 2, 3, 4, 5, "info", "warning", "error", "severe", "none", "NONE", "None", "NoNe"], ) def test_valid_report_levels(level: t.Any) -> None: # noqa: ANN401 """Test valid report levels accepted by docutils.""" result = config.RstcheckConfigFile(report_level=level) assert result is not None assert result.report_level is not None @staticmethod @pytest.mark.parametrize( "level", [-1, 0, 1.5, 6, 32, False, True, "information", "warn", "err", "critical", "fatal"], ) def test_invalid_report_levels(level: t.Any) -> None: # noqa: ANN401 """Test invalid report levels not accepted by docutils.""" with pytest.raises(TypeError, match="^Invalid report level$"): config.RstcheckConfigFile(report_level=level) class TestSplitStrValidatorMethod: """Test ``split_str`` validator method of the ``RstcheckConfig`` class. It validates the - ``ignore_directives`` - ``ignore_roles`` - ``ignore_substitutions`` - ``ignore_languages`` settings. """ @staticmethod def test_none_means_default() -> None: """Test ``None`` results in unset ignore_messages.""" result = config.RstcheckConfigFile( ignore_languages=None, ignore_directives=None, ignore_roles=None, ignore_substitutions=None, ) assert result is not None assert result.ignore_languages is None assert result.ignore_directives is None assert result.ignore_roles is None assert result.ignore_substitutions is None @staticmethod @pytest.mark.parametrize( ("string", "split_list"), [ ("value1", ["value1"]), ("value1,value2", ["value1", "value2"]), ("value1, value2", ["value1", "value2"]), ("value1 ,value2", ["value1", "value2"]), ("value1 , value2", ["value1", "value2"]), ("value1 ,\n value2", ["value1", "value2"]), ("value1 ,\n value2\n", ["value1", "value2"]), ("value1 , value2,", ["value1", "value2"]), ("value1 , value2 ,", ["value1", "value2"]), ("value1 , value2 , ", ["value1", "value2"]), ], ) def test_strings_are_transformed_to_lists(string: str, split_list: list[str]) -> None: """Test strings are split at the ",", trailing commas are ignored and whitespace cleaned.""" result = config.RstcheckConfigFile( ignore_languages=string, ignore_directives=string, ignore_roles=string, ignore_substitutions=string, ) assert result is not None assert result.ignore_languages == split_list assert result.ignore_directives == split_list assert result.ignore_roles == split_list assert result.ignore_substitutions == split_list @staticmethod @pytest.mark.parametrize( ("string_list", "string_list_cleaned"), [ (["value1"], ["value1"]), (["value1", "value2"], ["value1", "value2"]), (["value1", " value2"], ["value1", "value2"]), (["value1 ", "value2"], ["value1", "value2"]), (["value1 ", " value2"], ["value1", "value2"]), ], ) def test_string_lists_are_whitespace_cleaned( string_list: list[str], string_list_cleaned: list[str] ) -> None: """Test lists of strings are whitespace cleaned.""" result = config.RstcheckConfigFile( ignore_languages=string_list, ignore_directives=string_list, ignore_roles=string_list, ignore_substitutions=string_list, ) assert result is not None assert result.ignore_languages == string_list_cleaned assert result.ignore_directives == string_list_cleaned assert result.ignore_roles == string_list_cleaned assert result.ignore_substitutions == string_list_cleaned @staticmethod @pytest.mark.parametrize( "value", [ 1, [1], 1.1, [1.1], False, [False], True, [True], ["foo", 1], ], ) def test_invalid_settings(value: str) -> None: """Test invalid settings.""" with pytest.raises(TypeError, match="^Not a string or list of strings$"): config.RstcheckConfigFile( ignore_languages=value, ignore_directives=value, ignore_roles=value, ignore_substitutions=value, ) class TestJoinRegexStrValidatorMethod: """Test ``join_regex_str`` validator method of the ``RstcheckConfig`` class. It validates the ``ignore_messages`` setting. """ @staticmethod def test_none_means_unset() -> None: """Test ``None`` results in unset ignore_messages.""" result = config.RstcheckConfigFile(ignore_messages=None) assert result is not None assert result.ignore_messages is None @staticmethod def test_passed_regex_is_kept() -> None: """Test passing a regex.""" result = config.RstcheckConfigFile(ignore_messages=re.compile("")) assert result is not None assert isinstance(result.ignore_messages, re.Pattern) @staticmethod def test_strings_are_parsed_as_regex() -> None: """Test strings are parsed as regex.""" string = r"\d{4}\.[A-Z]+Test$" regex = re.compile(string) result = config.RstcheckConfigFile(ignore_messages=string) assert result is not None assert result.ignore_messages == regex @staticmethod def test_empty_strings_are_valid() -> None: """Test empty strings are parsed as regex too.""" string = "" regex = re.compile(string) result = config.RstcheckConfigFile(ignore_messages=string) assert result is not None assert result.ignore_messages == regex @staticmethod def test_string_list_are_joined_and_parsed_as_regex() -> None: """Test ignore_messages string lists are joined with "|" and parsed as regex.""" string_list = [r"\d{4}\.[A-Z]+Test$", r"\d{4}\.[A-Z]+Test2$", r"\d{4}\.[A-Z]+Test3$"] full_string = r"\d{4}\.[A-Z]+Test$|\d{4}\.[A-Z]+Test2$|\d{4}\.[A-Z]+Test3$" regex = re.compile(full_string) result = config.RstcheckConfigFile(ignore_messages=string_list) assert result is not None assert result.ignore_messages == regex @staticmethod @pytest.mark.parametrize( ("string_list", "full_string"), [([""], ""), (["", ""], "|"), ([], "")] ) def test_list_with_empty_contents(string_list: list[str], full_string: str) -> None: """Test list with empty contents are parsed as regex too.""" regex = re.compile(full_string) result = config.RstcheckConfigFile(ignore_messages=string_list) assert result is not None assert result.ignore_messages == regex @staticmethod @pytest.mark.parametrize( "value", [ 1, [1], 1.1, [1.1], False, [False], True, [True], ["foo", 1], ], ) def test_invalid_settings(value: str) -> None: """Test invalid ignore_messages settings.""" with pytest.raises(TypeError, match="^Not a string or list of strings$"): config.RstcheckConfigFile(ignore_messages=value) class TestIniFileLoader: """Test ``load_config_from_ini_file``.""" @staticmethod def test_no_config_on_none() -> None: """Test no config is loaded if NONE is set.""" conf_file = pathlib.Path("NONE") result = config._load_config_from_ini_file(conf_file) assert result is None @staticmethod def test_missing_file_errors(tmp_path: pathlib.Path) -> None: """Test ``FileNotFoundError`` is raised on missing file.""" conf_file = tmp_path / "config.ini" with pytest.raises(FileNotFoundError): config._load_config_from_ini_file(conf_file) @staticmethod def test_not_a_file_errors(tmp_path: pathlib.Path) -> None: """Test ``FileNotFoundError`` is raised when not file.""" conf_file = tmp_path with pytest.raises(FileNotFoundError): config._load_config_from_ini_file(conf_file) @staticmethod def test_returns_none_on_missing_section(tmp_path: pathlib.Path) -> None: """Test ``None`` is returned on missing section.""" conf_file = tmp_path / "config.ini" file_content = "[not-rstcheck]" conf_file.write_text(file_content) result = config._load_config_from_ini_file(conf_file) assert result is None @staticmethod def test_ignores_unsupported_settings(tmp_path: pathlib.Path) -> None: """Test unsupported settings are ignored.""" conf_file = tmp_path / "config.ini" file_content = """[rstcheck] unsupported_feature=True """ conf_file.write_text(file_content) result = config._load_config_from_ini_file(conf_file) assert result is not None @staticmethod def test_supported_settings_are_loaded(tmp_path: pathlib.Path) -> None: """Test supported settings are loaded.""" conf_file = tmp_path / "config.ini" file_content = """[rstcheck] report_level=3 ignore_directives=directive ignore_roles=role ignore_substitutions=substitution ignore_languages=language ignore_messages=message """ conf_file.write_text(file_content) regex = re.compile("message") result = config._load_config_from_ini_file(conf_file) assert result is not None assert result.report_level == config.ReportLevel.ERROR assert result.ignore_directives == ["directive"] assert result.ignore_roles == ["role"] assert result.ignore_substitutions == ["substitution"] assert result.ignore_languages == ["language"] assert result.ignore_messages == regex @staticmethod def test_multiline_strings(tmp_path: pathlib.Path) -> None: """Test multiline strings are parsed.""" conf_file = tmp_path / "config.ini" file_content = """[rstcheck] ignore_directives=directive ignore_roles= role1, role2, role3 """ conf_file.write_text(file_content) result = config._load_config_from_ini_file(conf_file) assert result is not None assert result.ignore_directives == ["directive"] assert result.ignore_roles == ["role1", "role2", "role3"] @staticmethod def test_multiline_strings_trailing_comma(tmp_path: pathlib.Path) -> None: """Test multiline strings with trailing comma are parsed.""" conf_file = tmp_path / "config.ini" file_content = """[rstcheck] ignore_directives=directive ignore_roles= role1, role2, role3, """ conf_file.write_text(file_content) result = config._load_config_from_ini_file(conf_file) assert result is not None assert result.ignore_directives == ["directive"] assert result.ignore_roles == ["role1", "role2", "role3"] @staticmethod def test_file_with_mixed_supported_settings(tmp_path: pathlib.Path) -> None: """Test mix of supported and unsupported settings.""" conf_file = tmp_path / "config.ini" file_content = """[rstcheck] report_level=3 ignore_directives=directive unsupported_feature=True """ conf_file.write_text(file_content) result = config._load_config_from_ini_file(conf_file) assert result is not None assert result.report_level == config.ReportLevel.ERROR assert result.ignore_directives == ["directive"] @staticmethod def test_file_with_mixed_supported_sections(tmp_path: pathlib.Path) -> None: """Test mix of rstcheck and other section.""" conf_file = tmp_path / "config.ini" file_content = """[rstcheck] report_level=3 ignore_directives=directive unsupported_feature=True [not-rstcheck] report_level=2 ignore_directives=not-directive unsupported_feature=False """ conf_file.write_text(file_content) result = config._load_config_from_ini_file(conf_file) assert result is not None assert result.report_level == config.ReportLevel.ERROR assert result.ignore_directives == ["directive"] @staticmethod def test_warning_is_logged_on_missing_section_by_default( tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture ) -> None: """Test if a warning is logged on missing section.""" conf_file = tmp_path / "config.ini" file_content = "[checkrst]" conf_file.write_text(file_content) result = config._load_config_from_ini_file(conf_file) assert result is None assert f"Config file has no [rstcheck] section: '{conf_file}'." in caplog.text @staticmethod def test_warning_on_missing_section_can_be_info( tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture ) -> None: """Test if the warning logged on missing section can be an info.""" conf_file = tmp_path / "config.ini" file_content = "[checkrst]" conf_file.write_text(file_content) caplog.set_level(logging.INFO) result = config._load_config_from_ini_file(conf_file, log_missing_section_as_warning=False) assert result is None assert f"Config file has no [rstcheck] section: '{conf_file}'." in caplog.text @staticmethod def test_dont_warning_on_unknown_settings_by_default( tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture ) -> None: """Test that no warning is logged on unknown setting by default.""" conf_file = tmp_path / "config.ini" file_content = """ [rstcheck] report_level=INFO unkwown_setting=true """ conf_file.write_text(file_content) result = config._load_config_from_ini_file( conf_file, log_missing_section_as_warning=False, ) assert result is not None assert not caplog.text @staticmethod def test_warning_on_unknown_settings_when_set( tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture ) -> None: """Test that a warning is logged on unknown setting when activated.""" conf_file = tmp_path / "config.ini" file_content = """ [rstcheck] report_level=INFO unkwown_setting=true """ conf_file.write_text(file_content) result = config._load_config_from_ini_file( conf_file, log_missing_section_as_warning=False, warn_unknown_settings=True, ) assert result is not None assert ( f"Unknown setting(s) ['unkwown_setting'] found in file: '{conf_file}'." in caplog.text ) @staticmethod def test_no_warning_on_known_settings_when_set( tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture ) -> None: """Test that a warning is logged on known settings when activated.""" conf_file = tmp_path / "config.ini" file_content = """ [rstcheck] report_level=INFO """ conf_file.write_text(file_content) result = config._load_config_from_ini_file( conf_file, log_missing_section_as_warning=False, warn_unknown_settings=True, ) assert result is not None assert not caplog.text @pytest.mark.skipif(not _extras.TOMLI_INSTALLED, reason="Depends on toml extra.") class TestTomlFileLoader: """Test ``load_config_from_toml_file``.""" @staticmethod def test_no_config_on_none() -> None: """Test no config is loaded if NONE is set.""" conf_file = pathlib.Path("NONE") result = config._load_config_from_toml_file(conf_file) assert result is None @staticmethod def test_wrong_file_suffix_errors(tmp_path: pathlib.Path) -> None: """Test ``ValueError`` is raised on wrong file suffix.""" conf_file = tmp_path / "config.ini" conf_file.touch() with pytest.raises(ValueError, match="^File is not a TOML file$"): config._load_config_from_toml_file(conf_file) @staticmethod def test_missing_file_errors(tmp_path: pathlib.Path) -> None: """Test ``FileNotFoundError`` is raised on missing file.""" conf_file = tmp_path / "config.toml" with pytest.raises(FileNotFoundError): config._load_config_from_toml_file(conf_file) @staticmethod def test_not_a_file_errors(tmp_path: pathlib.Path) -> None: """Test ``FileNotFoundError`` is raised when not file.""" conf_file = tmp_path / "config.toml" conf_file.mkdir() with pytest.raises(FileNotFoundError): config._load_config_from_toml_file(conf_file) @staticmethod @pytest.mark.parametrize("invalid_section", ["[tool.not-rstcheck]", "[rstcheck]"]) def test_returns_none_on_missing_section(tmp_path: pathlib.Path, invalid_section: str) -> None: """Test ``None`` is returned on missing section.""" conf_file = tmp_path / "config.toml" conf_file.write_text(invalid_section) result = config._load_config_from_toml_file(conf_file) assert result is None @staticmethod def test_ignores_unsupported_settings(tmp_path: pathlib.Path) -> None: """Test unsupported settings are ignored.""" conf_file = tmp_path / "config.toml" file_content = """[tool.rstcheck] unsupported_feature = true """ conf_file.write_text(file_content) result = config._load_config_from_toml_file(conf_file) assert result is not None @staticmethod def test_supported_settings_are_loaded(tmp_path: pathlib.Path) -> None: """Test supported settings are loaded.""" conf_file = tmp_path / "config.toml" file_content = """[tool.rstcheck] report_level = 3 ignore_directives = ["directive"] ignore_roles = ["role"] ignore_substitutions = ["substitution"] ignore_languages = ["language"] ignore_messages = "message" """ conf_file.write_text(file_content) regex = re.compile("message") result = config._load_config_from_toml_file(conf_file) assert result is not None assert result.report_level == config.ReportLevel.ERROR assert result.ignore_directives == ["directive"] assert result.ignore_roles == ["role"] assert result.ignore_substitutions == ["substitution"] assert result.ignore_languages == ["language"] assert result.ignore_messages == regex @staticmethod def test_file_with_mixed_supported_settings(tmp_path: pathlib.Path) -> None: """Test mix of supported and unsupported settings.""" conf_file = tmp_path / "config.toml" file_content = """[tool.rstcheck] report_level = 3 ignore_directives = ["directive"] unsupported_feature = true """ conf_file.write_text(file_content) result = config._load_config_from_toml_file(conf_file) assert result is not None assert result.report_level == config.ReportLevel.ERROR assert result.ignore_directives == ["directive"] @staticmethod def test_file_with_mixed_supported_sections(tmp_path: pathlib.Path) -> None: """Test mix of rstcheck and other section.""" conf_file = tmp_path / "config.toml" file_content = """[tool.rstcheck] report_level = 3 ignore_directives = ["directive"] unsupported_feature = true [tool.not-rstcheck] report_level = 2 ignore_directives = "not-directive" unsupported_feature = false """ conf_file.write_text(file_content) result = config._load_config_from_toml_file(conf_file) assert result is not None assert result.report_level == config.ReportLevel.ERROR assert result.ignore_directives == ["directive"] @staticmethod @pytest.mark.parametrize( ("value", "parsed_value"), [ ("none", config.ReportLevel.NONE), ("ERROR", config.ReportLevel.ERROR), ("1", config.ReportLevel.INFO), ("3", config.ReportLevel.ERROR), ], ) def test_report_level_as_strings( tmp_path: pathlib.Path, value: str, parsed_value: config.ReportLevel ) -> None: """Test report setting with string values.""" conf_file = tmp_path / "config.toml" file_content = f"""[tool.rstcheck] report_level = "{value}" """ conf_file.write_text(file_content) result = config._load_config_from_toml_file(conf_file) assert result is not None assert result.report_level == parsed_value @staticmethod @pytest.mark.parametrize( ("value", "parsed_value"), [ (1, config.ReportLevel.INFO), (3, config.ReportLevel.ERROR), (5, config.ReportLevel.NONE), ], ) def test_report_level_as_int( tmp_path: pathlib.Path, value: int, parsed_value: config.ReportLevel ) -> None: """Test report setting with integer values.""" conf_file = tmp_path / "config.toml" file_content = f"""[tool.rstcheck] report_level = {value} """ conf_file.write_text(file_content) result = config._load_config_from_toml_file(conf_file) assert result is not None assert result.report_level == parsed_value @staticmethod def test_ignore_messages_as_str(tmp_path: pathlib.Path) -> None: """Test ignore_messages setting with string value.""" conf_file = tmp_path / "config.toml" file_content = """[tool.rstcheck] ignore_messages = "some-regex" """ conf_file.write_text(file_content) regex = re.compile("some-regex") result = config._load_config_from_toml_file(conf_file) assert result is not None assert result.ignore_messages == regex @staticmethod def test_ignore_messages_as_list(tmp_path: pathlib.Path) -> None: """Test ignore_messages setting with list of strings value.""" conf_file = tmp_path / "config.toml" file_content = """[tool.rstcheck] ignore_messages = ["some-regex", "another-regex"] """ conf_file.write_text(file_content) regex = re.compile(r"some-regex|another-regex") result = config._load_config_from_toml_file(conf_file) assert result is not None assert result.ignore_messages == regex @staticmethod def test_warning_is_logged_on_missing_section_by_default( tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture ) -> None: """Test if a warning is logged on missing section.""" conf_file = tmp_path / "config.toml" file_content = "[tool.checkrst]" conf_file.write_text(file_content) result = config._load_config_from_toml_file(conf_file) assert result is None assert f"Config file has no [tool.rstcheck] section: '{conf_file}'." in caplog.text @staticmethod def test_warning_on_missing_section_can_be_info( tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture ) -> None: """Test if the warning logged on missing section can be an info.""" conf_file = tmp_path / "config.toml" file_content = "[tool.checkrst]" conf_file.write_text(file_content) caplog.set_level(logging.INFO) result = config._load_config_from_toml_file(conf_file, log_missing_section_as_warning=False) assert result is None assert f"Config file has no [tool.rstcheck] section: '{conf_file}'." in caplog.text @staticmethod def test_dont_warning_on_unknown_settings_by_default( tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture ) -> None: """Test that no warning is logged on unknown setting by default.""" conf_file = tmp_path / "config.toml" file_content = """ [tool.rstcheck] report_level="INFO" unkwown_setting=true """ conf_file.write_text(file_content) result = config._load_config_from_toml_file( conf_file, log_missing_section_as_warning=False, ) assert result is not None assert not caplog.text @staticmethod def test_warning_on_unknown_settings_when_set( tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture ) -> None: """Test that a warning is logged on unknown setting when activated.""" conf_file = tmp_path / "config.toml" file_content = """ [tool.rstcheck] report_level="INFO" unkwown_setting=true """ conf_file.write_text(file_content) result = config._load_config_from_toml_file( conf_file, log_missing_section_as_warning=False, warn_unknown_settings=True, ) assert result is not None assert ( f"Unknown setting(s) ['unkwown_setting'] found in file: '{conf_file}'." in caplog.text ) @staticmethod def test_no_warning_on_known_settings_when_set( tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture ) -> None: """Test that a warning is logged on known settings when activated.""" conf_file = tmp_path / "config.toml" file_content = """ [tool.rstcheck] report_level="INFO" """ conf_file.write_text(file_content) result = config._load_config_from_toml_file( conf_file, log_missing_section_as_warning=False, warn_unknown_settings=True, ) assert result is not None assert not caplog.text class TestConfigFileLoader: """Test ``load_config_file``.""" @staticmethod def test_no_config_on_none() -> None: """Test no config is loaded if NONE is set.""" conf_file = pathlib.Path("NONE") result = config.load_config_file(conf_file) assert result is None @staticmethod @pytest.mark.parametrize("ini_file", [".rstcheck.cfg", "setup.cfg", "config.ini", "config.cfg"]) def test_ini_files(tmp_path: pathlib.Path, ini_file: str) -> None: """Test with INI files.""" conf_file = tmp_path / ini_file file_content = """[rstcheck] report_level = 3 """ conf_file.write_text(file_content) result = config.load_config_file(conf_file) assert result is not None assert result.report_level == config.ReportLevel.ERROR @staticmethod @pytest.mark.skipif(not _extras.TOMLI_INSTALLED, reason="Depends on toml extra.") @pytest.mark.parametrize("toml_file", [".rstcheck.toml", "setup.toml", "config.toml"]) def test_toml_files(tmp_path: pathlib.Path, toml_file: str) -> None: """Test with TOML files.""" conf_file = tmp_path / toml_file file_content = """[tool.rstcheck] report_level = 3 """ conf_file.write_text(file_content) result = config.load_config_file(conf_file) assert result is not None assert result.report_level == config.ReportLevel.ERROR class TestConfigDirLoader: """Test ``load_config_file_from_dir``.""" @staticmethod def test_no_config_on_none() -> None: """Test no config is loaded if NONE is set.""" conf_file = pathlib.Path("NONE") result = config.load_config_file_from_dir(conf_file) assert result is None @staticmethod @pytest.mark.parametrize("ini_file", [".rstcheck.cfg", "setup.cfg"]) def test_supported_ini_files(tmp_path: pathlib.Path, ini_file: str) -> None: """Test with supported INI files.""" conf_file = tmp_path / ini_file file_content = """[rstcheck] report_level = 3 """ conf_file.write_text(file_content) result = config.load_config_file_from_dir(tmp_path) assert result is not None assert result.report_level == config.ReportLevel.ERROR @staticmethod @pytest.mark.parametrize("ini_file", ["config.ini", "config.cfg"]) def test_unsupported_ini_files(tmp_path: pathlib.Path, ini_file: str) -> None: """Test with unsupported INI files.""" conf_file = tmp_path / ini_file file_content = """[rstcheck] report_level = 3 """ conf_file.write_text(file_content) result = config.load_config_file_from_dir(tmp_path) assert result is None @staticmethod @pytest.mark.skipif(not _extras.TOMLI_INSTALLED, reason="Depends on toml extra.") @pytest.mark.parametrize("toml_file", ["pyproject.toml"]) def test_supported_toml_files(tmp_path: pathlib.Path, toml_file: str) -> None: """Test with supported TOML files.""" conf_file = tmp_path / toml_file file_content = """[tool.rstcheck] report_level = 3 """ conf_file.write_text(file_content) result = config.load_config_file_from_dir(tmp_path) assert result is not None assert result.report_level == config.ReportLevel.ERROR @staticmethod @pytest.mark.skipif(not _extras.TOMLI_INSTALLED, reason="Depends on toml extra.") @pytest.mark.parametrize("toml_file", [".rstcheck.toml", "setup.toml", "config.toml"]) def test_unsupported_toml_files(tmp_path: pathlib.Path, toml_file: str) -> None: """Test with unsupported TOML files.""" conf_file = tmp_path / toml_file file_content = """[tool.rstcheck] report_level = 3 """ conf_file.write_text(file_content) result = config.load_config_file_from_dir(tmp_path) assert result is None @staticmethod def test_rstcheck_over_setup(tmp_path: pathlib.Path) -> None: """Test .rstcheck.cfg takes precedence over setup.cfg.""" setup_conf_file = tmp_path / "setup.cfg" setup_file_content = """[rstcheck] report_level = 2 """ setup_conf_file.write_text(setup_file_content) rstcheck_conf_file = tmp_path / ".rstcheck.cfg" rstcheck_file_content = """[rstcheck] report_level = 3 """ rstcheck_conf_file.write_text(rstcheck_file_content) result = config.load_config_file_from_dir(tmp_path) assert result is not None assert result.report_level == config.ReportLevel.ERROR @staticmethod @pytest.mark.skipif(not _extras.TOMLI_INSTALLED, reason="Depends on toml extra.") def test_rstcheck_over_pyproject(tmp_path: pathlib.Path) -> None: """Test .rstcheck.cfg takes precedence over pyproject.toml.""" pyproject_conf_file = tmp_path / "pyproject.toml" pyproject_file_content = """[tool.rstcheck] report_level = 2 """ pyproject_conf_file.write_text(pyproject_file_content) rstcheck_conf_file = tmp_path / ".rstcheck.cfg" rstcheck_file_content = """[rstcheck] report_level = 3 """ rstcheck_conf_file.write_text(rstcheck_file_content) result = config.load_config_file_from_dir(tmp_path) assert result is not None assert result.report_level == config.ReportLevel.ERROR @staticmethod @pytest.mark.skipif(not _extras.TOMLI_INSTALLED, reason="Depends on toml extra.") def test_pyproject_over_setup(tmp_path: pathlib.Path) -> None: """Test pyproject.toml takes precedence over setup.cfg.""" setup_conf_file = tmp_path / "setup.cfg" setup_file_content = """[rstcheck] report_level = 2 """ setup_conf_file.write_text(setup_file_content) pyproject_conf_file = tmp_path / "pyproject.toml" pyproject_file_content = """[tool.rstcheck] report_level = 3 """ pyproject_conf_file.write_text(pyproject_file_content) result = config.load_config_file_from_dir(tmp_path) assert result is not None assert result.report_level == config.ReportLevel.ERROR @staticmethod @pytest.mark.skipif(not _extras.TOMLI_INSTALLED, reason="Depends on toml extra.") def test_missing_config_means_next_file_is_checked(tmp_path: pathlib.Path) -> None: """Test missing config in file results in checking of next file.""" setup_conf_file = tmp_path / "setup.cfg" setup_file_content = """[rstcheck] report_level = 3 """ setup_conf_file.write_text(setup_file_content) rstcheck_conf_file = tmp_path / ".rstcheck.cfg" rstcheck_file_content = """[not-rstcheck] report_level = 2 """ rstcheck_conf_file.write_text(rstcheck_file_content) result = config.load_config_file_from_dir(tmp_path) assert result is not None assert result.report_level == config.ReportLevel.ERROR @staticmethod def test_info_is_logged_on_no_config_found( tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture ) -> None: """Test if an info is logged if no config could be found.""" conf_dir = tmp_path caplog.set_level(logging.INFO) result = config.load_config_file_from_dir(conf_dir) assert result is None assert ( f"No config section in supported config files found in directory: '{conf_dir}'." in caplog.text ) @staticmethod def test_warning_is_logged_on_no_config_section_in_rstcheck_file( tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture ) -> None: """Test if an warning is logged if no config section could be found in .rstcheck.cfg.""" conf_dir = tmp_path conf_file = conf_dir / ".rstcheck.cfg" file_content = "[checkrst]" conf_file.write_text(file_content) result = config.load_config_file_from_dir(conf_dir, log_missing_section_as_warning=False) assert result is None assert f"Config file has no [rstcheck] section: '{conf_file}'." in caplog.text class TestConfigDirTreeLoader: """Test ``load_config_file_from_dir_tree``.""" @staticmethod def test_no_config_on_none() -> None: """Test no config is loaded if NONE is set.""" conf_file = pathlib.Path("NONE") result = config.load_config_file_from_dir_tree(conf_file) assert result is None @staticmethod def test_parent_searching(tmp_path: pathlib.Path) -> None: """Test option to search up the dir tree.""" nested_dir = tmp_path / "nested" nested_dir.mkdir() unsupported_file = nested_dir / "config.cfg" unsupported_file_content = """[rstcheck] report_level = 2 """ unsupported_file.write_text(unsupported_file_content) supported_file = tmp_path / "setup.cfg" supported_file_content = """[rstcheck] report_level = 3 """ supported_file.write_text(supported_file_content) result = config.load_config_file_from_dir_tree(nested_dir) assert result is not None assert result.report_level == config.ReportLevel.ERROR @staticmethod def test_no_file_up_to_root(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> None: """Test option to search up the dir tree with no file up to root dir.""" root_dir = tmp_path nested_dir = tmp_path / "nested" nested_dir.mkdir() monkeypatch.setattr(pathlib.Path, "parent", root_dir) result = config.load_config_file_from_dir_tree(nested_dir) assert result is None @staticmethod def test_info_is_logged_on_no_config_found( tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture ) -> None: """Test if an info is logged if no config could be found.""" conf_dir = tmp_path caplog.set_level(logging.INFO) monkeypatch.setattr(pathlib.Path, "parent", conf_dir) result = config.load_config_file_from_dir_tree(conf_dir) assert result is None assert ( f"No config section in supported config files found in directory tree: '{conf_dir}'." in caplog.text ) class TestConfigPathLoader: """Test ``load_config_file_from_path``.""" @staticmethod def test_raises_on_nonexisting_path() -> None: """Test raises FileNotFoundError on path that is not a file or directory.""" conf_file = pathlib.Path("does-not-exist-cfg") with pytest.raises(FileNotFoundError, match="Passed config path not found"): config.load_config_file_from_path(conf_file) @staticmethod def test_no_config_on_none() -> None: """Test no config is loaded if NONE is set.""" conf_file = pathlib.Path("NONE") result = config.load_config_file_from_path(conf_file) assert result is None @staticmethod def test_with_file(tmp_path: pathlib.Path) -> None: """Test with INI file.""" conf_file = tmp_path / ".rstcheck.cfg" file_content = """[rstcheck] report_level = 3 """ conf_file.write_text(file_content) result = config.load_config_file_from_path(conf_file) assert result is not None assert result.report_level == config.ReportLevel.ERROR @staticmethod def test_with_dir(tmp_path: pathlib.Path) -> None: """Test with directory.""" conf_file = tmp_path / ".rstcheck.cfg" file_content = """[rstcheck] report_level = 3 """ conf_file.write_text(file_content) result = config.load_config_file_from_path(tmp_path) assert result is not None assert result.report_level == config.ReportLevel.ERROR @staticmethod def test_with_nested_dir(tmp_path: pathlib.Path) -> None: """Test with nested dir tree.""" nested_dir = tmp_path / "nested" nested_dir.mkdir() conf_file = tmp_path / "setup.cfg" file_content = """[rstcheck] report_level = 3 """ conf_file.write_text(file_content) result = config.load_config_file_from_path(nested_dir, search_dir_tree=True) assert result is not None assert result.report_level == config.ReportLevel.ERROR class TestConfigMerger: """Test ``merge_configs``.""" @staticmethod def test_default_merge_with_full_config() -> None: """Test config merging with full config.""" config_base = config.RstcheckConfig(report_level=config.ReportLevel.SEVERE) config_add = config.RstcheckConfig(report_level=config.ReportLevel.ERROR) result = config.merge_configs(config_base, config_add) assert result.report_level == config.ReportLevel.ERROR @staticmethod def test_default_merge_with_file_config() -> None: """Test config merging with file config.""" config_base = config.RstcheckConfig(report_level=config.ReportLevel.SEVERE) config_add = config.RstcheckConfigFile(report_level=config.ReportLevel.ERROR) result = config.merge_configs(config_base, config_add) assert result.report_level == config.ReportLevel.ERROR @staticmethod def test_default_merge_with_full_config_and_empty_base_config() -> None: """Test config merging with full config and empty base config.""" config_base = config.RstcheckConfig() config_add = config.RstcheckConfig(report_level=config.ReportLevel.ERROR) result = config.merge_configs(config_base, config_add) assert result.report_level == config.ReportLevel.ERROR @staticmethod def test_default_merge_with_file_config_and_empty_base_config() -> None: """Test config merging with file config and empty base config.""" config_base = config.RstcheckConfig() config_add = config.RstcheckConfigFile(report_level=config.ReportLevel.ERROR) result = config.merge_configs(config_base, config_add) assert result.report_level == config.ReportLevel.ERROR @staticmethod def test_default_merge_with_full_config_and_changed_dominance() -> None: """Test config merging with full config and changed dominance.""" config_base = config.RstcheckConfig(report_level=config.ReportLevel.SEVERE) config_add = config.RstcheckConfig(report_level=config.ReportLevel.ERROR) result = config.merge_configs(config_base, config_add, config_add_is_dominant=False) assert result.report_level == config.ReportLevel.SEVERE @staticmethod def test_default_merge_with_file_config_and_changed_dominance() -> None: """Test config merging with file config and changed dominance.""" config_base = config.RstcheckConfig(report_level=config.ReportLevel.SEVERE) config_add = config.RstcheckConfigFile(report_level=config.ReportLevel.ERROR) result = config.merge_configs(config_base, config_add, config_add_is_dominant=False) assert result.report_level == config.ReportLevel.SEVERE @staticmethod def test_default_merge_with_full_config_and_changed_dominance_and_empty_base_config() -> None: """Test config merging with full config, changed dominance and empty base config. Test ``None`` gets overwritten by set value. """ config_base = config.RstcheckConfig() config_add = config.RstcheckConfig(report_level=config.ReportLevel.ERROR) result = config.merge_configs(config_base, config_add, config_add_is_dominant=False) assert result.report_level == config.ReportLevel.ERROR @staticmethod def test_default_merge_with_file_config_and_changed_dominance_and_empty_base_config() -> None: """Test config merging with file config, changed dominance and empty base config. Test ``None`` gets overwritten by set value. """ config_base = config.RstcheckConfig() config_add = config.RstcheckConfigFile(report_level=config.ReportLevel.ERROR) result = config.merge_configs(config_base, config_add, config_add_is_dominant=False) assert result.report_level == config.ReportLevel.ERROR rstcheck-core-1.2.1/tests/conftest.py000066400000000000000000000022511457751767200176350ustar00rootroot00000000000000"""Fixtures for tests.""" from __future__ import annotations import pathlib import typing as t import pytest from rstcheck_core import _extras REPO_DIR = pathlib.Path(__file__).resolve().parents[1].resolve() TESTING_DIR = REPO_DIR / "testing" EXAMPLES_DIR = TESTING_DIR / "examples" @pytest.fixture(name="patch_docutils_directives_and_roles_dict") def _patch_docutils_directives_and_roles_dict_fixture(monkeypatch: pytest.MonkeyPatch) -> None: """Monkeypatch docutils' directives and roles state dicts. This patch is required when tests are run in parallel (default), because they would under the hood all write to the same state dicts otherwise and influence each other. """ test_dict_directives: dict[str, t.Any] = {} test_dict_roles: dict[str, t.Any] = {} if _extras.SPHINX_INSTALLED: monkeypatch.setattr("sphinx.util.docutils.directives._directives", test_dict_directives) monkeypatch.setattr("sphinx.util.docutils.roles._roles", test_dict_roles) else: monkeypatch.setattr("docutils.parsers.rst.directives._directives", test_dict_directives) monkeypatch.setattr("docutils.parsers.rst.roles._roles", test_dict_roles) rstcheck-core-1.2.1/tests/inline_config_test.py000066400000000000000000000204571457751767200216620ustar00rootroot00000000000000"""Tests for ``inline_config`` module.""" from __future__ import annotations import typing as t import pytest from rstcheck_core import inline_config, types class TestInlineConfigGetter: """Test ``get_inline_config_from_source`` function.""" @staticmethod def test_empty_string_source() -> None: """Test giving an empty string as source results in no config found.""" source = "" result = list(inline_config.get_inline_config_from_source(source, "")) assert not result @staticmethod def test_source_without_config() -> None: """Test giving source without config comment results in no config found.""" source = """ Example ======= """ result = list(inline_config.get_inline_config_from_source(source, "")) assert not result @staticmethod def test_source_with_correct_config() -> None: """Test giving source with correct config comment results in config found.""" source = """ Example ======= .. rstcheck: ignore-languages=cpp """ result = list(inline_config.get_inline_config_from_source(source, "")) assert result == [types.InlineConfig(key="ignore-languages", value="cpp")] @staticmethod def test_source_with_unknown_config() -> None: """Test unknown config in comments is ignored.""" source = """ Example ======= .. rstcheck: unknown-config=true .. rstcheck: ignore_languages=json """ result = list(inline_config.get_inline_config_from_source(source, "")) assert not result @staticmethod def test_source_with_invalid_config_format_is_ignored() -> None: """Test unknown config in comments is ignored.""" source = """ Example ======= .. rstcheck: ignore-languages: cpp """ result = list(inline_config.get_inline_config_from_source(source, "")) assert not result @staticmethod def test_source_with_known_and_unknown_config() -> None: """Test unknown config comments are ignored when mixed with valid confg.""" source = """ Example ======= .. rstcheck: unknown-config=true .. rstcheck: ignore-languages=cpp """ result = list(inline_config.get_inline_config_from_source(source, "")) assert result == [types.InlineConfig(key="ignore-languages", value="cpp")] @staticmethod def test_source_with_correct_config_multiple_values() -> None: """Test giving source with correct config comment results in values found. Test with multiple values on one comment. """ source = """ Example ======= .. rstcheck: ignore-languages=cpp,json """ result = list(inline_config.get_inline_config_from_source(source, "")) assert result == [types.InlineConfig(key="ignore-languages", value="cpp,json")] @staticmethod def test_source_with_correct_config_multiple_comments() -> None: """Test giving source with correct config comment results in configs found. Test with multiple configs on different comments. """ source = """ Example ======= .. rstcheck: ignore-languages=cpp .. rstcheck: ignore-languages=json """ result = list(inline_config.get_inline_config_from_source(source, "")) assert result == [ types.InlineConfig(key="ignore-languages", value="cpp"), types.InlineConfig(key="ignore-languages", value="json"), ] @staticmethod def test_source_with_correct_config_whitespace() -> None: """Test giving source with correct config comment results in configs found. Test whitspace around equal sign. """ source = """ Example ======= .. rstcheck: ignore-languages =cpp .. rstcheck: ignore-languages= json .. rstcheck: ignore-languages = python """ result = list(inline_config.get_inline_config_from_source(source, "")) assert result == [ types.InlineConfig(key="ignore-languages", value="cpp"), types.InlineConfig(key="ignore-languages", value="json"), types.InlineConfig(key="ignore-languages", value="python"), ] @staticmethod def test_source_with_multiple_correct_configs() -> None: """Test source with multiple correct configs. Test whitspace around equal sign. """ source = """ Example ======= .. rstcheck: ignore-languages=cpp .. rstcheck: ignore-roles=role .. rstcheck: ignore-languages=python .. rstcheck: ignore-directives=direct """ result = list(inline_config.get_inline_config_from_source(source, "")) assert result == [ types.InlineConfig(key="ignore-languages", value="cpp"), types.InlineConfig(key="ignore-roles", value="role"), types.InlineConfig(key="ignore-languages", value="python"), types.InlineConfig(key="ignore-directives", value="direct"), ] class TestConfigFilterAndSpliter: """Test ``_filter_config_and_split_values`` function.""" @staticmethod def test_only_specified_config_is_used() -> None: """Test only specified config is returned.""" source = """ Example ======= .. rstcheck: unknown-config=true .. rstcheck: ignore-languages=cpp .. rstcheck: ignore-directives=directive1 """ result = list( inline_config._filter_config_and_split_values("ignore-languages", source, "") ) assert result == ["cpp"] @staticmethod def test_configs_are_comma_splitted() -> None: """Test configs are split on comma.""" source = """ Example ======= .. rstcheck: ignore-languages=cpp,json .. rstcheck: ignore-languages=python """ result = list( inline_config._filter_config_and_split_values("ignore-languages", source, "") ) assert result == ["cpp", "json", "python"] class FindIgnoreFn(t.Protocol): # noqa: D101 def __call__( # noqa: D102 self, source: str, source_origin: types.SourceFileOrString, warn_unknown_settings: bool = False, # noqa: FBT002 ) -> t.Generator[str, None, None]: ... class TestFindIgnoredFunctions: """Test ``find_ignored_*`` function. - find_ignored_directives - find_ignored_roles - find_ignored_substitutions - find_ignored_languages """ @staticmethod @pytest.mark.parametrize( ("target_function", "expected_result"), [ (inline_config.find_ignored_directives, ["directive1"]), (inline_config.find_ignored_roles, ["role1"]), (inline_config.find_ignored_substitutions, ["substitution1"]), (inline_config.find_ignored_languages, ["cpp"]), ], ) def test_only_target_config_is_used( target_function: FindIgnoreFn, expected_result: str ) -> None: """Test only targeted configs are returned.""" source = """ Example ======= .. rstcheck: unknown-config=true .. rstcheck: ignore-directives=directive1 .. rstcheck: ignore-roles=role1 .. rstcheck: ignore-substitutions=substitution1 .. rstcheck: ignore-languages=cpp """ result = list(target_function(source, "")) assert result == expected_result @staticmethod @pytest.mark.parametrize( ("target_function", "expected_result"), [ (inline_config.find_ignored_directives, ["directive1", "directive3", "directive2"]), (inline_config.find_ignored_roles, ["role1", "role3", "role2"]), ( inline_config.find_ignored_substitutions, ["substitution1", "substitution3", "substitution2"], ), (inline_config.find_ignored_languages, ["cpp", "json", "python"]), ], ) def test_values_are_comma_splitted(target_function: FindIgnoreFn, expected_result: str) -> None: """Test config values are split on comma.""" source = """ Example ======= .. rstcheck: unknown-config=true .. rstcheck: ignore-directives=directive1,directive3 .. rstcheck: ignore-directives=directive2 .. rstcheck: ignore-roles=role1,role3 .. rstcheck: ignore-roles=role2 .. rstcheck: ignore-substitutions=substitution1,substitution3 .. rstcheck: ignore-substitutions=substitution2 .. rstcheck: ignore-languages=cpp,json .. rstcheck: ignore-languages=python """ result = list(target_function(source, "")) assert result == expected_result rstcheck-core-1.2.1/tests/integration_tests/000077500000000000000000000000001457751767200212035ustar00rootroot00000000000000rstcheck-core-1.2.1/tests/integration_tests/__init__.py000066400000000000000000000000001457751767200233020ustar00rootroot00000000000000rstcheck-core-1.2.1/tests/integration_tests/conftest.py000066400000000000000000000002071457751767200234010ustar00rootroot00000000000000"""Fixtures for integration tests.""" from __future__ import annotations import re ERROR_CODE_REGEX = re.compile(r"\([A-Z]*?/\d\)") rstcheck-core-1.2.1/tests/integration_tests/file_checker_test.py000066400000000000000000000361571457751767200252330ustar00rootroot00000000000000"""Integration test for the main runner.""" from __future__ import annotations import io import pathlib import re import sys import typing as t import pytest from rstcheck_core import _extras, checker, config from tests.conftest import EXAMPLES_DIR class TestInput: """Test file input with good and bad files and piping.""" @staticmethod @pytest.mark.parametrize("test_file", list(EXAMPLES_DIR.glob("good/*.rst"))) def test_all_good_examples(test_file: pathlib.Path) -> None: """Test all files in ``testing/examples/good`` are errorless.""" if sys.platform == "win32" and test_file.name == "bom.rst": pytest.xfail(reason="BOM test fails for windows") init_config = config.RstcheckConfig() result = checker.check_file(test_file, init_config) assert not result @staticmethod @pytest.mark.parametrize("test_file", list(EXAMPLES_DIR.glob("bad/*.rst"))) def test_all_bad_examples(test_file: pathlib.Path) -> None: """Test all files in ``testing/examples/bad`` have errors.""" if sys.platform == "win32" and test_file.name == "bash.rst": pytest.xfail(reason="Unknown Windows specific wrong result for bash.rst") init_config = config.RstcheckConfig() result = checker.check_file(test_file, init_config) assert len(result) > 0 @staticmethod def test_good_example_with_piping(monkeypatch: pytest.MonkeyPatch) -> None: """Test good example file piped into rstcheck.""" test_file_pipe = EXAMPLES_DIR / "good" / "rst.rst" test_file_pipe_content = test_file_pipe.read_text("utf-8") monkeypatch.setattr(sys, "stdin", io.StringIO(test_file_pipe_content)) init_config = config.RstcheckConfig() result = checker.check_file(pathlib.Path("-"), init_config) assert not result @staticmethod def test_bad_example_with_piping(monkeypatch: pytest.MonkeyPatch) -> None: """Test bad example file piped into rstcheck.""" test_file_pipe = EXAMPLES_DIR / "bad" / "rst.rst" test_file_pipe_content = test_file_pipe.read_text("utf-8") monkeypatch.setattr(sys, "stdin", io.StringIO(test_file_pipe_content)) init_config = config.RstcheckConfig() result = checker.check_file(pathlib.Path("-"), init_config) assert len(result) == 1 class TestIgnoreOptions: """Test ignore_* options and report_level.""" @staticmethod def test_without_report_exits_zero() -> None: """Test bad example without report is ok.""" test_file = EXAMPLES_DIR / "bad" / "rst.rst" init_config = config.RstcheckConfig(report_level="none") result = checker.check_file(test_file, init_config) assert not result @staticmethod def test_ignore_language_silences_error() -> None: """Test bad example with ignored language is ok.""" test_file = EXAMPLES_DIR / "bad" / "cpp.rst" init_config = config.RstcheckConfig(ignore_languages="cpp") result = checker.check_file(test_file, init_config) assert not result @staticmethod def test_matching_ignore_msg_exits_zero() -> None: """Test matching ignore message.""" test_file = EXAMPLES_DIR / "bad" / "rst.rst" init_config = config.RstcheckConfig( ignore_messages=r"(Title .verline & underline mismatch\.$)" ) result = checker.check_file(test_file, init_config) assert not result @staticmethod def test_non_matching_ignore_msg_errors() -> None: """Test non matching ignore message.""" test_file = EXAMPLES_DIR / "bad" / "rst.rst" init_config = config.RstcheckConfig(ignore_messages=r"(No match\.$)") result = checker.check_file(test_file, init_config) assert len(result) == 1 @staticmethod def test_table_substitution_error_fixed_by_ignore() -> None: """Test that ignored substitutions in tables are correctly handled.""" test_file = EXAMPLES_DIR / "bad" / "table_substitutions.rst" init_config = config.RstcheckConfig(ignore_substitutions="FOO_ID,BAR_ID") result = checker.check_file(test_file, init_config) assert not result class TestWithoutConfigFile: """Test without config file in dir tree.""" @staticmethod def test_error_without_config_file() -> None: """Test bad example without set config file and implicit config file shows errors.""" test_file = EXAMPLES_DIR / "without_configuration" / "bad.rst" init_config = config.RstcheckConfig() result = checker.check_file(test_file, init_config) assert len(result) == 6 @staticmethod def test_no_error_with_set_ini_config_file() -> 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" file_config = t.cast(config.RstcheckConfigFile, config.load_config_file(config_file)) init_config = config.RstcheckConfig(config_path=config_file, **file_config.model_dump()) result = checker.check_file(test_file, init_config) assert not result @staticmethod def test_no_error_with_set_config_dir() -> 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" file_config = t.cast( config.RstcheckConfigFile, config.load_config_file_from_dir(config_dir) ) init_config = config.RstcheckConfig(config_path=config_dir, **file_config.model_dump()) result = checker.check_file(test_file, init_config) assert not result @staticmethod @pytest.mark.skipif(not _extras.TOMLI_INSTALLED, reason="Depends on toml extra.") def test_no_error_with_set_toml_config_file() -> 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" file_config = t.cast(config.RstcheckConfigFile, config.load_config_file(config_file)) init_config = config.RstcheckConfig(config_path=config_file, **file_config.model_dump()) result = checker.check_file(test_file, init_config) assert not result class TestWithConfigFile: """Test with config file in dir tree.""" @staticmethod def test_file_1_is_bad_without_config() -> None: """Test bad file ``bad.rst`` without config file is not ok.""" test_file = EXAMPLES_DIR / "with_configuration" / "bad.rst" init_config = config.RstcheckConfig(config_path=pathlib.Path("NONE")) result = checker.check_file(test_file, init_config) assert len(result) == 6 @staticmethod def test_file_2_is_bad_without_config() -> None: """Test bad file ``bad_rst.rst`` without config file not ok.""" test_file = EXAMPLES_DIR / "with_configuration" / "bad_rst.rst" init_config = config.RstcheckConfig(config_path=pathlib.Path("NONE")) result = checker.check_file(test_file, init_config) assert len(result) == 2 @staticmethod def test_bad_file_1_with_implicit_config_no_errors() -> None: """Test bad file ``bad.rst`` with implicit config file is ok.""" test_file = EXAMPLES_DIR / "with_configuration" / "bad.rst" init_config = config.RstcheckConfig() result = checker.check_file(test_file, init_config) assert not result @staticmethod def test_bad_file_2_with_implicit_config_some_errors() -> None: """Test bad file ``bad_rst.rst`` with implicit config file partially ok.""" test_file = EXAMPLES_DIR / "with_configuration" / "bad_rst.rst" init_config = config.RstcheckConfig() result = checker.check_file(test_file, init_config) assert len(result) == 1 @staticmethod def test_bad_file_1_with_explicit_config_no_errors() -> 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" file_config = t.cast(config.RstcheckConfigFile, config.load_config_file(config_file)) init_config = config.RstcheckConfig(config_path=config_file, **file_config.model_dump()) result = checker.check_file(test_file, init_config) assert not result @staticmethod def test_bad_file_2_with_explicit_config_some_errors() -> 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" file_config = t.cast(config.RstcheckConfigFile, config.load_config_file(config_file)) init_config = config.RstcheckConfig(config_path=config_file, **file_config.model_dump()) result = checker.check_file(test_file, init_config) assert len(result) == 1 class TestCustomDirectivesAndRoles: """Test custom directives and roles.""" @staticmethod def test_custom_directive_and_role() -> None: """Test file with custom directive and role.""" test_file = EXAMPLES_DIR / "custom" / "custom_directive_and_role.rst" init_config = config.RstcheckConfig() result = checker.check_file(test_file, init_config) assert len(result) == 4 @staticmethod def test_custom_directive_and_role_with_ignore() -> None: """Test file with custom directive and role and CLI ignores.""" test_file = EXAMPLES_DIR / "custom" / "custom_directive_and_role.rst" init_config = config.RstcheckConfig( ignore_directives="custom-directive", ignore_roles="custom-role" ) result = checker.check_file(test_file, init_config) assert not result @staticmethod def test_custom_directive_and_role_with_config_file() -> 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" file_config = t.cast(config.RstcheckConfigFile, config.load_config_file(config_file)) init_config = config.RstcheckConfig(config_path=config_file, **file_config.model_dump()) result = checker.check_file(test_file, init_config) assert not result 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() -> None: """Test sphinx example errors without sphinx.""" test_file = EXAMPLES_DIR / "sphinx" / "good.rst" init_config = config.RstcheckConfig() result = checker.check_file(test_file, init_config) assert len(result) == 2 @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") def test_sphinx_role_exits_zero_with_sphinx() -> None: """Test sphinx example does not error with sphinx.""" test_file = EXAMPLES_DIR / "sphinx" / "good.rst" init_config = config.RstcheckConfig() result = checker.check_file(test_file, init_config) assert not result class TestInlineIgnoreComments: """Test inline config comments to ignore things.""" @staticmethod def test_bad_example_has_issues() -> None: """Test all issues are found on bad example.""" test_file = EXAMPLES_DIR / "inline_config" / "without_inline_ignore.rst" init_config = config.RstcheckConfig() result = checker.check_file(test_file, init_config) assert len(result) == 6 err_msgs = " ".join([e["message"] for e in result]) assert "custom-directive" in err_msgs assert "custom-role" in err_msgs assert "python" in err_msgs assert "unmatched-substitution" in err_msgs @staticmethod def test_bad_example_has_no_issues_with_inline_ignores() -> None: """Test no issues are found on bad example with ignore comments.""" test_file = EXAMPLES_DIR / "inline_config" / "with_inline_ignore.rst" init_config = config.RstcheckConfig() result = checker.check_file(test_file, init_config) assert not result class TestInlineFlowControlComments: """Test inline flow control comments to e.g. skip things.""" @staticmethod @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() -> 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" init_config = config.RstcheckConfig() result = checker.check_file(test_file, init_config) assert len(result) == 1 err_msgs = " ".join([e["message"] for e in result]) assert len(re.findall(r"unexpected EOF while parsing", err_msgs)) == 1 @staticmethod @pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires python3.10 or higher") def test_bad_example_has_only_one_issue() -> 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" init_config = config.RstcheckConfig() result = checker.check_file(test_file, init_config) assert len(result) == 1 err_msgs = " ".join([e["message"] for e in result]) assert len(re.findall(r"'\(' was never closed", err_msgs)) == 1 @staticmethod @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() -> 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" init_config = config.RstcheckConfig() result = checker.check_file(test_file, init_config) assert len(result) == 1 err_msgs = " ".join([e["message"] for e in result]) assert len(re.findall(r"unexpected EOF while parsing", err_msgs)) == 1 @staticmethod @pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires python3.10 or higher") def test_nested_bad_example_has_only_one_issue() -> 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" init_config = config.RstcheckConfig() result = checker.check_file(test_file, init_config) assert len(result) == 1 err_msgs = " ".join([e["message"] for e in result]) assert len(re.findall(r"'\(' was never closed", err_msgs)) == 1 rstcheck-core-1.2.1/tests/integration_tests/runner_test.py000066400000000000000000000572511457751767200241370ustar00rootroot00000000000000"""Integration test for the main runner.""" from __future__ import annotations import io import pathlib import re import sys import pytest from rstcheck_core import _extras, config, runner from tests.conftest import EXAMPLES_DIR from tests.integration_tests.conftest import ERROR_CODE_REGEX class TestInput: """Test file input with good and bad files and piping.""" @staticmethod @pytest.mark.parametrize("test_file", list(EXAMPLES_DIR.glob("good/*.rst"))) def test_all_good_examples(test_file: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None: """Test all files in ``testing/examples/good`` are errorless.""" if sys.platform == "win32" and test_file.name == "bom.rst": pytest.xfail(reason="BOM test fails for windows") init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out @staticmethod @pytest.mark.xfail( sys.platform == "win32", reason="Random unknown Windows specific wrong result", strict=False ) def test_all_good_examples_recurively(capsys: pytest.CaptureFixture[str]) -> None: """Test all files in ``testing/examples/good`` recursively.""" test_dir = EXAMPLES_DIR / "good" init_config = config.RstcheckConfig(recursive=True) _runner = runner.RstcheckMainRunner(check_paths=[test_dir], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out @staticmethod @pytest.mark.parametrize("test_file", list(EXAMPLES_DIR.glob("bad/*.rst"))) def test_all_bad_examples(test_file: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None: """Test all files in ``testing/examples/bad`` have errors.""" if sys.platform == "win32" and test_file.name == "bash.rst": pytest.xfail(reason="Unknown Windows specific wrong result for bash.rst") init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result != 0 assert ERROR_CODE_REGEX.search(capsys.readouterr().err) is not None @staticmethod def test_all_bad_examples_recurively(capsys: pytest.CaptureFixture[str]) -> None: """Test all files in ``testing/examples/bad`` recursively.""" test_dir = EXAMPLES_DIR / "bad" init_config = config.RstcheckConfig(recursive=True) _runner = runner.RstcheckMainRunner(check_paths=[test_dir], rstcheck_config=init_config) result = _runner.run() assert result != 0 assert ERROR_CODE_REGEX.search(capsys.readouterr().err) is not None @staticmethod def test_mix_of_good_and_bad_examples(capsys: pytest.CaptureFixture[str]) -> None: """Test mix of good and bad examples.""" test_file_good = EXAMPLES_DIR / "good" / "rst.rst" test_file_bad = EXAMPLES_DIR / "bad" / "rst.rst" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner( check_paths=[test_file_good, test_file_bad], rstcheck_config=init_config ) result = _runner.run() assert result != 0 assert ERROR_CODE_REGEX.search(capsys.readouterr().err) is not None @staticmethod def test_good_example_with_piping( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: """Test good example file piped into rstcheck.""" test_file_pipe = EXAMPLES_DIR / "good" / "rst.rst" test_file_pipe_content = test_file_pipe.read_text("utf-8") monkeypatch.setattr(sys, "stdin", io.StringIO(test_file_pipe_content)) init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner( check_paths=[pathlib.Path("-")], rstcheck_config=init_config ) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out @staticmethod def test_bad_example_with_piping( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: """Test bad example file piped into rstcheck.""" test_file_pipe = EXAMPLES_DIR / "bad" / "rst.rst" test_file_pipe_content = test_file_pipe.read_text("utf-8") monkeypatch.setattr(sys, "stdin", io.StringIO(test_file_pipe_content)) init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner( check_paths=[pathlib.Path("-")], rstcheck_config=init_config ) result = _runner.run() assert result != 0 assert len(ERROR_CODE_REGEX.findall(capsys.readouterr().err)) == 1 @staticmethod def test_piping_with_additional_files_results_in_nonexisting_file( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture, ) -> None: """Test piping into rstcheck is ignored with additional files.""" test_file = EXAMPLES_DIR / "good" / "rst.rst" test_file_pipe = EXAMPLES_DIR / "bad" / "rst.rst" test_file_pipe_content = test_file_pipe.read_text("utf-8") monkeypatch.setattr(sys, "stdin", io.StringIO(test_file_pipe_content)) init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner( check_paths=[pathlib.Path("-"), test_file], rstcheck_config=init_config ) result = _runner.run() assert result != 0 assert "Error! Issues detected." in capsys.readouterr().err assert "Path does not exist or is not a file: '-'." in caplog.text class TestIgnoreOptions: """Test ignore_* options and report_level.""" @staticmethod def test_without_report_exits_zero(capsys: pytest.CaptureFixture[str]) -> None: """Test bad example without report is ok.""" test_file = EXAMPLES_DIR / "bad" / "rst.rst" init_config = config.RstcheckConfig(report_level="none") _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out @staticmethod def test_ignore_language_silences_error(capsys: pytest.CaptureFixture[str]) -> None: """Test bad example with ignored language is ok.""" test_file = EXAMPLES_DIR / "bad" / "cpp.rst" init_config = config.RstcheckConfig(ignore_languages="cpp") _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out @staticmethod def test_matching_ignore_msg_exits_zero(capsys: pytest.CaptureFixture[str]) -> None: """Test matching ignore message.""" test_file = EXAMPLES_DIR / "bad" / "rst.rst" init_config = config.RstcheckConfig( ignore_messages=r"(Title .verline & underline mismatch\.$)" ) _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out @staticmethod def test_non_matching_ignore_msg_errors(capsys: pytest.CaptureFixture[str]) -> None: """Test non matching ignore message.""" test_file = EXAMPLES_DIR / "bad" / "rst.rst" init_config = config.RstcheckConfig(ignore_messages=r"(No match\.$)") _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result != 0 assert len(ERROR_CODE_REGEX.findall(capsys.readouterr().err)) == 1 @staticmethod def test_table_substitution_error_fixed_by_ignore(capsys: pytest.CaptureFixture[str]) -> None: """Test that ignored substitutions in tables are correctly handled.""" test_file = EXAMPLES_DIR / "bad" / "table_substitutions.rst" init_config = config.RstcheckConfig(ignore_substitutions="FOO_ID,BAR_ID") _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out class TestWithoutConfigFile: """Test without config file in dir tree.""" @staticmethod def test_error_without_config_file(capsys: pytest.CaptureFixture[str]) -> None: """Test bad example without set config file and implicit config file shows errors.""" test_file = EXAMPLES_DIR / "without_configuration" / "bad.rst" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result != 0 assert len(ERROR_CODE_REGEX.findall(capsys.readouterr().err)) == 6 @staticmethod def test_no_error_with_set_ini_config_file(capsys: pytest.CaptureFixture[str]) -> 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" init_config = config.RstcheckConfig(config_path=config_file) _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out @staticmethod def test_no_error_with_set_config_dir(capsys: pytest.CaptureFixture[str]) -> 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" init_config = config.RstcheckConfig(config_path=config_dir) _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out @staticmethod @pytest.mark.skipif(not _extras.TOMLI_INSTALLED, reason="Depends on toml extra.") def test_no_error_with_set_toml_config_file(capsys: pytest.CaptureFixture[str]) -> 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" init_config = config.RstcheckConfig(config_path=config_file) _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out class TestWithConfigFile: """Test with config file in dir tree.""" @staticmethod def test_file_1_is_bad_without_config(capsys: pytest.CaptureFixture[str]) -> 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") init_config = config.RstcheckConfig(config_path=config_file) _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result != 0 assert len(ERROR_CODE_REGEX.findall(capsys.readouterr().err)) == 6 @staticmethod def test_file_2_is_bad_without_config(capsys: pytest.CaptureFixture[str]) -> 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") init_config = config.RstcheckConfig(config_path=config_file) _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result != 0 assert len(ERROR_CODE_REGEX.findall(capsys.readouterr().err)) == 2 @staticmethod def test_bad_file_1_with_implicit_config_no_errors(capsys: pytest.CaptureFixture[str]) -> None: """Test bad file ``bad.rst`` with implicit config file is ok.""" test_file = EXAMPLES_DIR / "with_configuration" / "bad.rst" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out @staticmethod def test_bad_file_2_with_implicit_config_some_errors( capsys: pytest.CaptureFixture[str], ) -> None: """Test bad file ``bad_rst.rst`` with implicit config file partially ok.""" test_file = EXAMPLES_DIR / "with_configuration" / "bad_rst.rst" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result != 0 assert len(ERROR_CODE_REGEX.findall(capsys.readouterr().err)) == 1 @staticmethod def test_bad_file_1_with_explicit_config_no_errors(capsys: pytest.CaptureFixture[str]) -> 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" init_config = config.RstcheckConfig(config_path=config_file) _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out @staticmethod def test_bad_file_2_with_explicit_config_some_errors( capsys: pytest.CaptureFixture[str], ) -> 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" init_config = config.RstcheckConfig(config_path=config_file) _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result != 0 assert len(ERROR_CODE_REGEX.findall(capsys.readouterr().err)) == 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, caplog: pytest.LogCaptureFixture, capsys: pytest.CaptureFixture[str], ) -> 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 init_config = config.RstcheckConfig(config_path=config_file) _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out assert "Unknown setting(s)" not in caplog.text @staticmethod @pytest.mark.parametrize("config_file_name", ["bad_config.cfg", "bad_config.toml"]) def test_no_warnings_are_logged_by_default_on_ini_files( config_file_name: str, caplog: pytest.LogCaptureFixture, capsys: pytest.CaptureFixture[str], ) -> 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 init_config = config.RstcheckConfig(config_path=config_file, warn_unknown_settings=True) _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out assert "Unknown setting(s)" in caplog.text class TestCustomDirectivesAndRoles: """Test custom directives and roles.""" @staticmethod def test_custom_directive_and_role(capsys: pytest.CaptureFixture[str]) -> None: """Test file with custom directive and role.""" test_file = EXAMPLES_DIR / "custom" / "custom_directive_and_role.rst" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result != 0 assert len(ERROR_CODE_REGEX.findall(capsys.readouterr().err)) == 4 @staticmethod def test_custom_directive_and_role_with_ignore(capsys: pytest.CaptureFixture[str]) -> None: """Test file with custom directive and role and CLI ignores.""" test_file = EXAMPLES_DIR / "custom" / "custom_directive_and_role.rst" init_config = config.RstcheckConfig( ignore_directives="custom-directive", ignore_roles="custom-role" ) _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out @staticmethod def test_custom_directive_and_role_with_config_file( capsys: pytest.CaptureFixture[str], ) -> 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" init_config = config.RstcheckConfig(config_path=config_file) _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out 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(capsys: pytest.CaptureFixture[str]) -> None: """Test sphinx example errors without sphinx.""" test_file = EXAMPLES_DIR / "sphinx" / "good.rst" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result != 0 assert len(ERROR_CODE_REGEX.findall(capsys.readouterr().err)) == 2 @staticmethod @pytest.mark.skipif(not _extras.SPHINX_INSTALLED, reason="Depends on sphinx extra.") def test_sphinx_role_exits_zero_with_sphinx(capsys: pytest.CaptureFixture[str]) -> None: """Test sphinx example does not error with sphinx.""" test_file = EXAMPLES_DIR / "sphinx" / "good.rst" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out class TestInlineIgnoreComments: """Test inline config comments to ignore things.""" @staticmethod def test_bad_example_has_issues(capsys: pytest.CaptureFixture[str]) -> None: """Test all issues are found on bad example.""" test_file = EXAMPLES_DIR / "inline_config" / "without_inline_ignore.rst" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result != 0 stderr = capsys.readouterr().err assert len(ERROR_CODE_REGEX.findall(stderr)) == 6 assert "custom-directive" in stderr assert "custom-role" in stderr assert "python" in stderr assert "unmatched-substitution" in stderr @staticmethod def test_bad_example_has_no_issues_with_inline_ignores( capsys: pytest.CaptureFixture[str], ) -> None: """Test no issues are found on bad example with ignore comments.""" test_file = EXAMPLES_DIR / "inline_config" / "with_inline_ignore.rst" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result == 0 assert "Success! No issues detected." in capsys.readouterr().out class TestInlineFlowControlComments: """Test inline flow control comments to e.g. skip things.""" @staticmethod @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(capsys: pytest.CaptureFixture[str]) -> 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" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result != 0 assert len(re.findall(r"unexpected EOF while parsing", capsys.readouterr().err)) == 1 @staticmethod @pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires python3.10 or higher") def test_bad_example_has_only_one_issue(capsys: pytest.CaptureFixture[str]) -> 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" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result != 0 assert len(re.findall(r"'\(' was never closed", capsys.readouterr().err)) == 1 @staticmethod @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( capsys: pytest.CaptureFixture[str], ) -> 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" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result != 0 assert len(re.findall(r"unexpected EOF while parsing", capsys.readouterr().err)) == 1 @staticmethod @pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires python3.10 or higher") def test_nested_bad_example_has_only_one_issue(capsys: pytest.CaptureFixture[str]) -> 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" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(check_paths=[test_file], rstcheck_config=init_config) result = _runner.run() assert result != 0 assert len(re.findall(r"'\(' was never closed", capsys.readouterr().err)) == 1 rstcheck-core-1.2.1/tests/runner_test.py000066400000000000000000000506031457751767200203640ustar00rootroot00000000000000"""Tests for ``runner`` module.""" from __future__ import annotations import contextlib import multiprocessing import pathlib import sys import typing as t from pathlib import Path import pytest from rstcheck_core import checker, config, runner, types if t.TYPE_CHECKING: import pytest_mock class TestRstcheckMainRunnerInit: """Test ``RstcheckMainRunner.__init__`` method.""" @staticmethod def test_load_config_file_if_set(mocker: pytest_mock.MockerFixture) -> None: """Test config file is loaded if set.""" mocked_loader = mocker.patch.object(runner.RstcheckMainRunner, "load_config_file") config_file_path = pathlib.Path("some-file") init_config = config.RstcheckConfig(config_path=config_file_path) runner.RstcheckMainRunner([], init_config) # act mocked_loader.assert_called_once_with(config_file_path, warn_unknown_settings=False) @staticmethod def test_no_load_config_file_if_unset(mocker: pytest_mock.MockerFixture) -> None: """Test no config file is loaded if unset.""" mocked_loader = mocker.patch.object(runner.RstcheckMainRunner, "load_config_file") init_config = config.RstcheckConfig() runner.RstcheckMainRunner([], init_config) # act mocked_loader.assert_not_called() @staticmethod @pytest.mark.skipif(sys.platform != "win32", reason="Windows specific.") @pytest.mark.parametrize("pool_size", [0, 1, 60, 61, 62, 100]) def test_max_pool_size_on_windows(pool_size: int, monkeypatch: pytest.MonkeyPatch) -> None: """Test pool size is 61 at max on windows.""" monkeypatch.setattr(multiprocessing, "cpu_count", lambda: pool_size) init_config = config.RstcheckConfig() result = runner.RstcheckMainRunner([], init_config)._pool_size assert result <= 61 class TestRstcheckMainRunnerConfigFileLoader: """Test ``RstcheckMainRunner.load_config_file`` method.""" @staticmethod def test_no_config_update_on_no_file_config(monkeypatch: pytest.MonkeyPatch) -> None: """Test config is not updated when no file config is found.""" monkeypatch.setattr( config, "load_config_file_from_path", lambda _, warn_unknown_settings: None ) init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner([], init_config) _runner.load_config_file(pathlib.Path()) # act assert _runner.config == init_config @staticmethod def test_config_update_on_found_file_config(monkeypatch: pytest.MonkeyPatch) -> None: """Test config is updated when file config is found.""" file_config = config.RstcheckConfigFile(report_level=config.ReportLevel.SEVERE) monkeypatch.setattr( config, "load_config_file_from_path", lambda _, warn_unknown_settings: file_config ) init_config = config.RstcheckConfig(report_level=config.ReportLevel.INFO) _runner = runner.RstcheckMainRunner([], init_config, overwrite_config=True) _runner.load_config_file(pathlib.Path()) # act assert _runner.config.report_level == config.ReportLevel.SEVERE class TestRstcheckMainRunnerFileListUpdater: """Test ``RstcheckMainRunner.update_file_list`` method.""" @staticmethod def test_empty_file_list() -> None: """Test empty file list results in no changes.""" file_list: list[pathlib.Path] = [] init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(file_list, init_config) _runner.update_file_list() # act assert not _runner._files_to_check @staticmethod def test_single_file_in_list(tmp_path: pathlib.Path) -> None: """Test single file in list results in only this file in the list.""" test_file = tmp_path / "rst.rst" test_file.touch() file_list = [test_file] init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(file_list, init_config) _runner.update_file_list() # act assert _runner._files_to_check == file_list @staticmethod def test_multiple_files_in_list(tmp_path: pathlib.Path) -> None: """Test multiple files in list results in only these files in the list.""" test_dir_1 = tmp_path / "one" test_dir_1.mkdir() test_dir_2 = tmp_path / "two" test_dir_2.mkdir() test_file1 = test_dir_1 / "rst.rst" test_file1.touch() test_file2 = test_dir_2 / "rst.rst" test_file2.touch() file_list = [test_file1, test_file2] init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(file_list, init_config) _runner.update_file_list() # act assert _runner._files_to_check == file_list @staticmethod def test_non_rst_files(tmp_path: pathlib.Path) -> None: """Test non rst files are filtered out.""" test_file1 = tmp_path / "rst.rst" test_file1.touch() test_file2 = tmp_path / "foo.bar" test_file2.touch() test_file3 = tmp_path / "rst.rst" test_file3.touch() file_list = [test_file1, test_file2, test_file3] init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(file_list, init_config) _runner.update_file_list() # act assert len(_runner._files_to_check) == 2 @staticmethod def test_directory_without_recursive(tmp_path: pathlib.Path) -> None: """Test directory without recusrive results in empty file list.""" test_file = tmp_path / "rst.rst" test_file.touch() file_list = [tmp_path] init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(file_list, init_config) _runner.update_file_list() # act assert not _runner._files_to_check @staticmethod def test_directory_with_recursive(tmp_path: pathlib.Path) -> None: """Test directory with recusrive results in directories files in file list.""" test_file1 = tmp_path / "rst.rst" test_file1.touch() test_file2 = tmp_path / "rst2.rst" test_file2.touch() file_list = [tmp_path] init_config = config.RstcheckConfig(recursive=True) _runner = runner.RstcheckMainRunner(file_list, init_config) _runner.update_file_list() # act assert len(_runner._files_to_check) == 2 assert tmp_path / "rst.rst" in _runner._files_to_check assert tmp_path / "rst2.rst" in _runner._files_to_check @staticmethod def test_dash_as_file() -> None: """Test dash as file.""" file_list = [pathlib.Path("-")] init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(file_list, init_config) _runner.update_file_list() # act assert file_list == _runner._files_to_check @staticmethod def test_dash_as_file_with_others(tmp_path: pathlib.Path) -> None: """Test dash as file with other files gets ignored.""" test_file = tmp_path / "rst.rst" test_file.touch() file_list = [pathlib.Path("-"), test_file] init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(file_list, init_config) _runner.update_file_list() # act assert len(_runner._files_to_check) == 1 assert test_file in _runner._files_to_check class TestRstcheckMainRunnerFileListFilter: """Test ``RstcheckMainRunner._filter_nonexisting`` method.""" @staticmethod def test_empty_file_list() -> None: """Test empty file list results in no changes.""" file_list: list[pathlib.Path] = [] init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(file_list, init_config) result = _runner._filter_nonexisting_paths(file_list) assert not result assert not _runner._nonexisting_paths @staticmethod def test_single_file_in_list(tmp_path: pathlib.Path) -> None: """Test single file in list results in only this file in the list.""" test_file = tmp_path / "rst.rst" test_file.touch() file_list = [test_file] init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(file_list, init_config) result = _runner._filter_nonexisting_paths(file_list) assert result == file_list assert not _runner._nonexisting_paths @staticmethod def test_multiple_files_in_list(tmp_path: pathlib.Path) -> None: """Test multiple files in list results in only these files in the list.""" test_dir_1 = tmp_path / "one" test_dir_1.mkdir() test_dir_2 = tmp_path / "two" test_dir_2.mkdir() test_file1 = test_dir_1 / "rst.rst" test_file1.touch() test_file2 = test_dir_2 / "rst.rst" test_file2.touch() file_list = [test_file1, test_file2] init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(file_list, init_config) result = _runner._filter_nonexisting_paths(file_list) assert result == file_list assert not _runner._nonexisting_paths @staticmethod def test_non_rst_files(tmp_path: pathlib.Path) -> None: """Test non rst files are not filtered out.""" test_file1 = tmp_path / "rst.rst" test_file1.touch() test_file2 = tmp_path / "foo.bar" test_file2.touch() test_file3 = tmp_path / "rst.rst" test_file3.touch() file_list = [test_file1, test_file2, test_file3] init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(file_list, init_config) result = _runner._filter_nonexisting_paths(file_list) assert result == file_list assert not _runner._nonexisting_paths @staticmethod def test_directory_without_recursive(tmp_path: pathlib.Path) -> None: """Test directory without recusrive results in empty file list.""" test_file = tmp_path / "rst.rst" test_file.touch() file_list = [tmp_path] init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(file_list, init_config) result = _runner._filter_nonexisting_paths(file_list) assert not result assert _runner._nonexisting_paths == file_list @staticmethod def test_directory_with_recursive(tmp_path: pathlib.Path) -> None: """Test directory with recusrive results in directories files in file list.""" test_file1 = tmp_path / "rst.rst" test_file1.touch() test_file2 = tmp_path / "rst2.rst" test_file2.touch() file_list = [tmp_path] init_config = config.RstcheckConfig(recursive=True) _runner = runner.RstcheckMainRunner(file_list, init_config) result = _runner._filter_nonexisting_paths(file_list) assert result == file_list assert not _runner._nonexisting_paths @staticmethod def test_dash_as_file() -> None: """Test dash as file gets filtered out.""" file_list = [pathlib.Path("-")] init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(file_list, init_config) result = _runner._filter_nonexisting_paths(file_list) assert not result assert _runner._nonexisting_paths == file_list @pytest.mark.parametrize( "lint_errors", [[], [types.LintError(source_origin="", line_number=0, message="message")]], ) def test__run_checks_sync_method( lint_errors: list[types.LintError], monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path ) -> None: """Test ``RstcheckMainRunner._run_checks_sync`` method. Test results are returned. """ monkeypatch.setattr(checker, "check_file", lambda _0, _1, _3: lint_errors) test_file1 = tmp_path / "rst.rst" test_file1.touch() test_file2 = tmp_path / "rst2.rst" test_file2.touch() file_list = [test_file1, test_file2] init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(file_list, init_config) result = _runner._run_checks_sync() assert len(result) == 2 assert len(result[0]) == len(lint_errors) assert len(result[1]) == len(lint_errors) @pytest.mark.parametrize( "lint_errors", [[], [types.LintError(source_origin="", line_number=0, message="message")]], ) def test__run_checks_parallel_method( lint_errors: list[types.LintError], monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path ) -> None: """Test ``RstcheckMainRunner._run_checks_parallel`` method. Test results are returned. The multiprocessing.Pool needs to be mocked, because it interferes with pytest-xdist. """ class MockedPool: """Mocked instance of ``multiprocessing.Pool``.""" @staticmethod def starmap(_0, _1) -> list[list[types.LintError]]: """Mock for ``multiprocessing.Pool.starmap`` method.""" return [lint_errors, lint_errors] @contextlib.contextmanager def mock_pool(_) -> t.Generator[MockedPool, None, None]: """Mock context manager for ``multiprocessing.Pool``.""" yield MockedPool() monkeypatch.setattr(multiprocessing, "Pool", mock_pool) test_file1 = tmp_path / "rst.rst" test_file1.touch() test_file2 = tmp_path / "rst2.rst" test_file2.touch() file_list = [test_file1, test_file2] init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner(file_list, init_config) result = _runner._run_checks_parallel() # act assert len(result) == 2 assert len(result[0]) == len(lint_errors) assert len(result[1]) == len(lint_errors) @pytest.mark.parametrize( ("results", "error_count"), [([], 0), ([[types.LintError(source_origin="", line_number=0, message="message")]], 1)], ) def test__update_results_method(results: list[list[types.LintError]], error_count: int) -> None: """Test ``RstcheckMainRunner._update_results`` method. Test results are set. """ init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner([], init_config) _runner._update_results(results) # act assert len(_runner.errors) == error_count def test_check_method_sync_with_1_file(mocker: pytest_mock.MockerFixture) -> None: """Test ``RstcheckMainRunner.check`` method. Test checks are run in sync for 1 file. """ mocked_sync_runner = mocker.patch.object(runner.RstcheckMainRunner, "_run_checks_sync") mocked_parallel_runner = mocker.patch.object(runner.RstcheckMainRunner, "_run_checks_parallel") init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner([], init_config) _runner._files_to_check = [pathlib.Path("file")] _runner.check() # act mocked_sync_runner.assert_called_once() mocked_parallel_runner.assert_not_called() def test_check_method_parallel_with_more_files(mocker: pytest_mock.MockerFixture) -> None: """Test ``RstcheckMainRunner.check`` method. Test checks are run in parallel for more file. """ mocked_sync_runner = mocker.patch.object(runner.RstcheckMainRunner, "_run_checks_sync") mocked_parallel_runner = mocker.patch.object(runner.RstcheckMainRunner, "_run_checks_parallel") init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner([], init_config) _runner._files_to_check = [pathlib.Path("file"), pathlib.Path("file2")] _runner.check() # act mocked_sync_runner.assert_not_called() mocked_parallel_runner.assert_called_once() class TestRstcheckMainRunnerResultPrinter: """Test ``RstcheckMainRunner.get_result`` method.""" @staticmethod def test_exit_code_on_success() -> None: """Test exit code 0 is returned on no erros.""" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner([], init_config) result = _runner.print_result() assert result == 0 @staticmethod def test_success_message_on_success(capsys: pytest.CaptureFixture[str]) -> None: """Test success message is printed to stdout by default if no errors.""" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner([], init_config) _runner.print_result() # act assert "Success! No issues detected." in capsys.readouterr().out @staticmethod def test_error_message_on_error( tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str] ) -> None: """Test error message is printed to stderr by default if errors are found.""" test_file = tmp_path / "nonexisting.rst" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner([test_file], init_config) _runner.print_result() # act assert "Error! Issues detected." in capsys.readouterr().err @staticmethod def test_success_message_print_to_file(tmp_path: pathlib.Path) -> None: """Test success message is printed to given file.""" out_file = tmp_path / "outfile.txt" out_file.touch() init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner([], init_config) # fmt: off with Path(out_file).open(encoding="utf-8", mode="w") as out_file_handle: _runner.print_result(output_file=out_file_handle) # act # fmt: on assert "Success! No issues detected." in out_file.read_text() @staticmethod def test_exit_code_on_error() -> None: """Test exit code 1 is returned when erros were found.""" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner([], init_config) _runner.errors = [ types.LintError(source_origin="", line_number=0, message="message") ] result = _runner.print_result() assert result == 1 @staticmethod def test_no_success_message_on_error(capsys: pytest.CaptureFixture[str]) -> None: """Test no succuess message is printed when erros were found.""" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner([], init_config) _runner.errors = [ types.LintError(source_origin="", line_number=0, message="message") ] _runner.print_result() # act assert "Success! No issues detected." not in capsys.readouterr() @staticmethod def test_error_printed_to_stderr_by_default(capsys: pytest.CaptureFixture[str]) -> None: """Test errors are printed to stderr.""" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner([], init_config) _runner._update_results( [[types.LintError(source_origin="", line_number=0, message="Some error.")]] ) _runner.print_result() # act assert "(ERROR/3) Some error" in capsys.readouterr().err @staticmethod def test_error_printed_to_file(tmp_path: pathlib.Path) -> None: """Test errors are printed to stderr.""" out_file = tmp_path / "outfile.txt" out_file.touch() init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner([], init_config) _runner._update_results( [[types.LintError(source_origin="", line_number=0, message="Some error.")]] ) # fmt: off with Path(out_file).open(encoding="utf-8", mode="w") as out_file_handle: _runner.print_result(output_file=out_file_handle) # act # fmt: on assert "(ERROR/3) Some error" in out_file.read_text() @staticmethod def test_error_category_prepend(capsys: pytest.CaptureFixture[str]) -> None: """Test ``(ERROR/3)`` is prepended when no category is present.""" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner([], init_config) _runner._update_results( [[types.LintError(source_origin="", line_number=0, message="Some error.")]] ) _runner.print_result() # act assert "(ERROR/3) Some error." in capsys.readouterr().err @staticmethod def test_error_message_format(capsys: pytest.CaptureFixture[str]) -> None: """Test error message format.""" init_config = config.RstcheckConfig() _runner = runner.RstcheckMainRunner([], init_config) _runner._update_results( [ [ types.LintError( source_origin="", line_number=0, message="(ERROR/3) Some error." ) ] ] ) _runner.print_result() # act assert ":0: (ERROR/3) Some error." in capsys.readouterr().err rstcheck-core-1.2.1/tests/types_test.py000066400000000000000000000021261457751767200202140ustar00rootroot00000000000000"""Tests for ``types`` module.""" from __future__ import annotations import re from rstcheck_core import types class TestIgnoreDictConstructor: """Test ``construct_ignore_dict`` function.""" @staticmethod def test_no_args() -> None: """Test construction of IgnoreDict with default values.""" result = types.construct_ignore_dict() assert result == types.IgnoreDict( messages=None, languages=[], directives=[], roles=[], substitutions=[], ) @staticmethod def test_all_args() -> None: """Test construction of IgnoreDict with set values.""" result = types.construct_ignore_dict( messages=re.compile("msg"), languages=["lang"], directives=["dir"], roles=["role"], substitutions=["sub"], ) assert result == types.IgnoreDict( messages=re.compile("msg"), languages=["lang"], directives=["dir"], roles=["role"], substitutions=["sub"], ) rstcheck-core-1.2.1/tox.ini000066400000000000000000000165131457751767200156150ustar00rootroot00000000000000[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,coverage,doctest,spelling} labels = test = py{312,311,310,39,38},py{312,311,310,39,38}-with-yaml,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-yaml,py38-with-sphinx{5,6,7},py38-with-tomli,coverage-all py3.9 = py39,py39-with-yaml,py39-with-sphinx{5,6,7},py39-with-tomli,coverage-all py3.10 = py310,py310-with-yaml,py310-with-sphinx{5,6,7},py310-with-tomli,package,coverage-all py3.11 = py311,py311-with-yaml,py311-with-sphinx{5,6,7},py311-with-tomli,package,coverage-all py3.12 = py312,py312-with-yaml,py312-with-sphinx{5,6,7},py312-with-tomli,package,coverage-all docs = docs-test-{html,coverage,doctest} docs-full = docs-test-{html,linkcheck,coverage,doctest,spelling} [testenv] description = basic config env base_python = python3.11 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 src/rstcheck_core mypy tests mypy docs/source/conf.py mypy prep_release.py [testenv:pylint] description = run pylint linter deps = pylint >= 2.12 extras = toml docs testing commands = pylint src/rstcheck_core 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-yaml] 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 yaml 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 yaml 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 = testing toml yaml 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,coverage,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 coverage: TOXENV_BUILDER = coverage 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}