├── .github ├── dependabot.yml └── workflows │ ├── dependabot.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── Makefile ├── README.md ├── mypy_clean_slate ├── __init__.py └── main.py ├── poetry.lock ├── pyproject.toml ├── scripts ├── __init__.py └── add_help_to_readme.py ├── setup.cfg └── tests ├── __init__.py ├── test_generate_readme.py └── test_mypy_clean_slate.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # GitHub Actions 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | commit-message: 14 | prefix: ⬆ 15 | # Python 16 | - package-ecosystem: "pip" 17 | directory: "/" 18 | schedule: 19 | interval: "daily" 20 | commit-message: 21 | prefix: ⬆ 22 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-approve 2 | on: pull_request_target 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v1.6.0 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'}} 20 | run: gh pr merge --auto --merge "$PR_URL" 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | 4 | jobs: 5 | linting: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Check out repository 9 | uses: actions/checkout@v4 10 | - name: Set up python "3.11" 11 | id: setup-python 12 | uses: actions/setup-python@v4 13 | with: 14 | python-version: "3.11" 15 | #---------------------------------------------- 16 | # ----- install & configure poetry ----- 17 | #---------------------------------------------- 18 | - name: Install Poetry 19 | uses: snok/install-poetry@v1 20 | with: 21 | virtualenvs-create: true 22 | virtualenvs-in-project: true 23 | installer-parallel: true 24 | #---------------------------------------------- 25 | # install dependencies if cache does not exist 26 | #---------------------------------------------- 27 | - name: Install dependencies 28 | run: | 29 | poetry install --no-interaction 30 | #---------------------------------------------- 31 | # install and run linters 32 | #---------------------------------------------- 33 | - name: ruff format 34 | run: poetry run ruff format . --check 35 | - name: ruff check 36 | run: poetry run ruff check . 37 | 38 | test: 39 | needs: linting 40 | strategy: 41 | fail-fast: true 42 | matrix: 43 | os: [ "ubuntu-latest" ] 44 | python-version: [ "3.9", "3.10", "3.11" ] 45 | runs-on: ${{ matrix.os }} 46 | steps: 47 | #---------------------------------------------- 48 | # check-out repo and set-up python 49 | #---------------------------------------------- 50 | - name: Check out repository 51 | uses: actions/checkout@v4 52 | - name: Set up python ${{ matrix.python-version }} 53 | id: setup-python 54 | uses: actions/setup-python@v4 55 | with: 56 | python-version: ${{ matrix.python-version }} 57 | #---------------------------------------------- 58 | # ----- install & configure poetry ----- 59 | #---------------------------------------------- 60 | - name: Install Poetry 61 | uses: snok/install-poetry@v1 62 | with: 63 | virtualenvs-create: true 64 | virtualenvs-in-project: true 65 | installer-parallel: true 66 | #---------------------------------------------- 67 | # install dependencies if cache does not exist 68 | #---------------------------------------------- 69 | - name: Install dependencies 70 | run: | 71 | poetry install --no-interaction 72 | #---------------------------------------------- 73 | # install your root project, if required 74 | #---------------------------------------------- 75 | - name: Install library 76 | run: poetry install --no-interaction 77 | #---------------------------------------------- 78 | # add matrix specifics and run test suite 79 | #---------------------------------------------- 80 | - name: Pytest ${{ matrix.python-version }} 81 | run: poetry run pytest 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | dist/ 4 | build/ 5 | *.egg-info/ 6 | .python-version 7 | .coverage 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-ast 7 | - id: check-case-conflict 8 | - id: check-docstring-first 9 | - id: check-executables-have-shebangs 10 | - id: check-merge-conflict 11 | - id: check-toml 12 | - id: check-vcs-permalinks 13 | - id: check-yaml 14 | - id: debug-statements 15 | - id: destroyed-symlinks 16 | - id: detect-private-key 17 | - id: end-of-file-fixer 18 | - id: fix-byte-order-marker 19 | - id: fix-encoding-pragma 20 | args: ["--remove"] 21 | - id: forbid-new-submodules 22 | - id: mixed-line-ending 23 | - id: trailing-whitespace 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 geo7 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean requirements 2 | .PHONY: git-stats git-log cloc clean-git 3 | .PHONY: deploy 4 | .PHONY: test 5 | .PHONY: requirements 6 | .PHONY: help 7 | 8 | CLOC := cloc 9 | 10 | ######### 11 | # UTILS # 12 | ######### 13 | 14 | help: 15 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort 16 | 17 | clean: 18 | @find . -type f -name "*.pyc" -delete 19 | @find . -type d -name "__pycache__" -exec rm -rf {} + 20 | @find . -type d -name ".pytest_cache" -exec rm -rf {} + 21 | @find . -type d -name ".mypy_cache" -exec rm -rf {} + 22 | @find . -type d -name ".ipynb_checkpoints" -exec rm -rf {} + 23 | 24 | cloc: 25 | @echo "Code statistics using cloc:" 26 | $(CLOC) --exclude-dir=venv . 27 | 28 | readme: 29 | poetry run python -m scripts.add_help_to_readme 30 | 31 | ######## 32 | # LINT # 33 | ######## 34 | 35 | pre-commit-run: 36 | poetry run pre-commit run --all-files 37 | 38 | mypy: 39 | poetry run mypy --strict . 40 | 41 | lint: mypy 42 | poetry run ruff check . 43 | poetry run ruff format . --check 44 | @$(MAKE) --no-print-directory clean 45 | 46 | format: pre-commit-run 47 | poetry run ruff format . 48 | poetry run ruff check . --fix 49 | @$(MAKE) --no-print-directory clean 50 | 51 | ########## 52 | # POETRY # 53 | ########## 54 | 55 | poetry.lock: 56 | poetry lock --no-update 57 | 58 | install: poetry.lock 59 | poetry install 60 | @$(MAKE) --no-print-directory clean 61 | 62 | ########## 63 | # PYTEST # 64 | ########## 65 | 66 | test: ## run tests 67 | poetry run pytest --cov=mypy_clean_slate --cov-report=html 68 | poetry run coverage html 69 | @$(MAKE) --no-print-directory clean 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mypy Clean Slate 2 | 3 | 4 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 5 | [![PyPI Latest Release](https://img.shields.io/pypi/v/mypy-clean-slate.svg)](https://pypi.org/project/mypy-clean-slate/) 6 | [![License](https://img.shields.io/pypi/l/mypy-clean-slate.svg)](https://github.com/geo7/mypy_clean_slate/blob/main/LICENSE) 7 | [![image](https://img.shields.io/pypi/pyversions/mypy-clean-slate.svg)](https://pypi.python.org/pypi/mypy-clean-slate) 8 | [![Actions status](https://github.com/geo7/mypy_clean_slate/workflows/CI/badge.svg)](https://github.com/geo7/mypy_clean_slate/actions) 9 | 10 | 11 | 12 | CLI tool for providing a clean slate for mypy usage within a project 13 | 14 | ## Motivation 15 | 16 | It can be difficult to get a large project to the point where `mypy --strict` 17 | can be run on it. Rather than incrementally increasing the severity of mypy, 18 | either overall or per module, `mypy_clean_slate` enables one to ignore all 19 | previous errors so that `mypy --strict` (or similar) can be used almost 20 | immediately. This enables all code written from that point on to be checked with 21 | `mypy --strict` (or whichever flags are preferred), gradually removing the 22 | `type: ignore` comments from that point onwards. 23 | 24 | Often running `mypy_clean_slate` will cover all errors cleanly in a single pass, 25 | but there are cases when not all error output is generated first time, and it 26 | can be necessary to run a couple of times, checking the diffs. Example of this 27 | scenario is given. 28 | 29 | By default `mypy_clean_slate` works by parsing the output of `mypy --strict` and 30 | adding the relevant `type: ignore[code]` to each line, though custom flags can 31 | be passed to mypy instead. Only errors from the report are considered, notes are 32 | not handled. Meaning something such as `error: Function is missing a type 33 | annotation [no-untyped-def]` will have `# type: ignore[no-untyped-def]` 34 | appended to the end of the line, whereas `note: (Skipping most remaining errors 35 | due to unresolved imports or missing stubs; fix these first)` will be ignored. 36 | Errors relating to unused ignores (which might occur if code changes after 37 | adding the initial ignore) can also be handled. 38 | 39 | # Installation 40 | 41 | ```bash 42 | pip install mypy-clean-slate 43 | ``` 44 | 45 | # Usage 46 | 47 | [comment]: # (CLI help split) 48 | 49 | ``` 50 | usage: mypy_clean_slate [options] 51 | 52 | CLI tool for providing a clean slate for mypy usage within a project. 53 | 54 | Default expectation is to want to get a project into a state that it 55 | will pass mypy when run with `--strict`, if this isn't the case custom 56 | flags can be passed to mypy via the `--mypy_flags` argument. 57 | 58 | options: 59 | -h, --help show this help message and exit 60 | -r, --generate_mypy_error_report 61 | Generate 'mypy_error_report.txt' in the cwd. 62 | -p PATH_TO_CODE, --path_to_code PATH_TO_CODE 63 | Where code is that needs report generating for it. 64 | -a, --add_type_ignore 65 | Add "# type: ignore[]" to suppress all raised mypy errors. 66 | --remove_unused Remove unused instances of "# type: ignore[]" if raised as an error by mypy. 67 | -o MYPY_REPORT_OUTPUT, --mypy_report_output MYPY_REPORT_OUTPUT 68 | File to save report output to (default is mypy_error_report.txt) 69 | --mypy_flags MYPY_FLAGS 70 | Custom flags to pass to mypy (provide them as a single string, default is to use --strict) 71 | ``` 72 | 73 | [comment]: # (CLI help split) 74 | 75 | See `./tests/test_mypy_clean_slate.py` for some examples with before/after. 76 | 77 | 78 | 79 | # Examples 80 | 81 | ## Simple example 82 | 83 | Given a project with only: 84 | 85 | ```txt 86 | ➜ simple_example git:(master) ✗ tree 87 | . 88 | `-- simple.py 89 | 90 | 0 directories, 1 file 91 | ``` 92 | 93 | Containing: 94 | 95 | ```python 96 | # simple.py 97 | def f(x): 98 | return x + 1 99 | ``` 100 | 101 | The report can be generated, and `simple.py` updated, using `mypy_clean_slate -ra`, resulting in: 102 | 103 | 104 | ```python 105 | def f(x): # type: ignore[no-untyped-def] 106 | return x + 1 107 | ``` 108 | 109 | And `mypy --strict` will now pass. 110 | 111 | ## Project example, using `pingouin` 112 | 113 | Project `pingouin` is located at: https://github.com/raphaelvallat/pingouin, and 114 | commit `ea8b5605a1776aaa0e89dd5c0e3df4320950fb38` is used for this example. 115 | `mypy_clean_slate` needs to be run a couple of times here. 116 | 117 | First, generate report and apply `type: ignore[]` 118 | 119 | ```sh 120 | mypy_clean_slate -ra 121 | ``` 122 | 123 | Looking at a subset of `git diff`: 124 | 125 | ```diff 126 | 127 | (venv) ➜ pingouin git:(master) ✗ git diff | grep 'type' | head 128 | +import sphinx_bootstrap_theme # type: ignore[import] 129 | +from outdated import warn_if_outdated # type: ignore[import] 130 | +import numpy as np # type: ignore[import] 131 | +from scipy.integrate import quad # type: ignore[import] 132 | + from scipy.special import gamma, betaln, hyp2f1 # type: ignore[import] 133 | + from mpmath import hyp3f2 # type: ignore[import] 134 | + from scipy.stats import binom # type: ignore[import] 135 | +import numpy as np # type: ignore[import] 136 | +from scipy.stats import norm # type: ignore[import] 137 | +import numpy as np # type: ignore[import] 138 | ``` 139 | 140 | Changes are added and committed with message `'mypy_clean_slate first pass'` (commit message used makes no functional difference), and the report re-generated: 141 | 142 | ```bash 143 | mypy_clean_slate -r 144 | ``` 145 | 146 | Which reports `Found 1107 errors in 39 files (checked 42 source files)`. So, re-running `mypy_clean_slate` 147 | 148 | ```bash 149 | mypy_clean_slate -a 150 | ``` 151 | 152 | And looking again at the diff: 153 | 154 | ```diff 155 | 156 | (venv) ➜ pingouin git:(master) ✗ gd | grep 'type' | head 157 | +latex_elements = { # type: ignore[var-annotated] 158 | +def setup(app): # type: ignore[no-untyped-def] 159 | @@ -27,4 +27,4 @@ from outdated import warn_if_outdated # type: ignore[import] 160 | +set_default_options() # type: ignore[no-untyped-call] 161 | +def _format_bf(bf, precision=3, trim='0'): # type: ignore[no-untyped-def] 162 | if type(bf) == str: 163 | +def bayesfactor_ttest(t, nx, ny=None, paired=False, tail='two-sided', r=.707): # type: ignore[no-untyped-def] 164 | + def fun(g, t, n, r, df): # type: ignore[no-untyped-def] 165 | +def bayesfactor_pearson(r, n, tail='two-sided', method='ly', kappa=1.): # type: ignore[no-untyped-def] 166 | + def fun(g, r, n): # type: ignore[no-untyped-def] 167 | ``` 168 | 169 | Committing these with `'mypy_clean_slate second pass'`, and re-running `mypy_clean_slate -r` outputs the following: 170 | 171 | ```txt 172 | (venv) ➜ pingouin git:(master) ✗ cat mypy_error_report.txt 173 | Success: no issues found in 42 source files 174 | ``` 175 | 176 | Can now rebase / amend commits as necessary, but could now update CI/pre-commit or whatever to use `mypy --strict` (or a subset of its flags) going forwards. 177 | 178 | 179 | # Handling of existing comments and `pylint` 180 | 181 | Lines which contain existing comments such as: 182 | 183 | ```python 184 | def ThisFunction(something): # pylint: disable=invalid-name 185 | return f"this is {something}" 186 | ``` 187 | 188 | Will be updated to: 189 | 190 | ```python 191 | def ThisFunction(something): # type: ignore[no-untyped-def] # pylint: disable=invalid-name 192 | return f"this is {something}" 193 | ``` 194 | 195 | As the `type:` comment needs to precede pylints. 196 | 197 | # Issues 198 | 199 | ## Generating report 200 | 201 | The report generation is pretty straightforward, `mypy . --strict --show-error-codes`, so might not be worth having as part of this script. The user can generate the report to a text file and just pass the path to that as an argument. 202 | 203 | ## Handling `-> None` 204 | 205 | Report output for functions which don't return is pretty consistent, so these could be automated if considered worth it. 206 | 207 | ## Integration with other tooling 208 | 209 | I've tried to consider `pylint` comments, but no doubt there are many other arguments for different tools which aren't taken into consideration. 210 | -------------------------------------------------------------------------------- /mypy_clean_slate/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | __version__ = "0.3.4" 4 | -------------------------------------------------------------------------------- /mypy_clean_slate/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import io 5 | import itertools 6 | import pathlib 7 | import re 8 | import shlex 9 | import subprocess 10 | import sys 11 | import textwrap 12 | import tokenize 13 | import warnings 14 | from typing import TYPE_CHECKING, TypeVar 15 | 16 | if TYPE_CHECKING: 17 | from collections.abc import Sequence 18 | 19 | 20 | T = TypeVar("T") 21 | # contains (file_path, line_number, error_code); file to update, line within that file to 22 | # append `type: ignore[]` 23 | FileUpdate = tuple[str, int, str] 24 | 25 | 26 | def raise_if_none(*, value: T | None) -> T: 27 | if value is None: 28 | msg = "None value" 29 | raise RuntimeError(msg) 30 | return value 31 | 32 | 33 | def generate_mypy_error_report( 34 | *, 35 | path_to_code: pathlib.Path, 36 | mypy_flags: list[str], 37 | ) -> str: 38 | """Run mypy and generate report with errors.""" 39 | no_arguments_passed = (len(mypy_flags) == 0) or ((len(mypy_flags) == 1) and mypy_flags[0] == "") 40 | 41 | if no_arguments_passed: 42 | # If no flags are passed we just assume we want to get things ready to 43 | # use with --strict going forwards. 44 | mypy_command = [ 45 | "mypy", 46 | f"{str(path_to_code)}", 47 | # Want error codes output from mypy to re-add in ignores. 48 | "--show-error-codes", 49 | # pretty output will format reports in an unexpected way for parsing. 50 | "--no-pretty", 51 | "--strict", # Default is to assume we want to aim for --strict. 52 | ] 53 | else: 54 | mypy_command = [ 55 | "mypy", 56 | f"{str(path_to_code)}", 57 | # Leaving --show-error-codes and --no-pretty as the error codes are 58 | # necessary to enable parsing the report output and writing back to 59 | # the files. --no-pretty is needed as, if there's a config setting 60 | # to use pretty the report output is altered and not parsed 61 | # properly. 62 | "--show-error-codes", 63 | "--no-pretty", 64 | *mypy_flags, 65 | ] 66 | 67 | print(f"Generating mypy report using: {' '.join(mypy_command)}") 68 | 69 | # Mypy is likely to return '1' here (otherwise pointless using this script) 70 | mypy_process = subprocess.run( # pylint: disable=subprocess-run-check 71 | mypy_command, 72 | capture_output=True, 73 | ) 74 | # don't think there's any need to check stderr 75 | return mypy_process.stdout.decode() 76 | 77 | 78 | def exit_if_no_errors( 79 | *, 80 | report: Sequence[str], 81 | ) -> None: 82 | # A report with no errors will contain this substring, so if this substring 83 | # exists there's nothing to be done. 84 | success_check = "Success: no issues found in" 85 | if any(report_line.startswith(success_check) for report_line in report): 86 | msg = ( 87 | "Generated mypy report contains line starting with: " 88 | f"{success_check}, so there's probably nothing that needs to be done. " 89 | "Full report: \n" 90 | f"{report}" 91 | ) 92 | raise SystemExit(msg) 93 | 94 | 95 | # --- Add ` # type: ignore[]` to lines which throw errors. 96 | 97 | 98 | def extract_code_comment(*, line: str) -> tuple[str, str]: 99 | """ 100 | Break line into code,comment if necessary. 101 | 102 | When there are lines containing ignores for tooling such as pylint the mypy ignore should be 103 | placed before the pylint disable. Therefore it's necessary to split lines into code,comment. 104 | """ 105 | # if '#' isn't in line then there's definitely no trailing code comment. 106 | if "#" not in line: 107 | return line, "" 108 | 109 | # generate_tokens wants a "callable returning a single line of input" 110 | reader = io.StringIO(line).readline 111 | 112 | # TODO(geo7): Handle multiline statements properly. 113 | # https://github.com/geo7/mypy_clean_slate/issues/114 114 | try: 115 | comment_tokens = [t for t in tokenize.generate_tokens(reader) if t.type == tokenize.COMMENT] 116 | except tokenize.TokenError as er: 117 | warnings.warn(f"TokenError encountered: {er} for line {line}.", UserWarning, stacklevel=2) 118 | return line, "" 119 | 120 | # If there's an inline comment then only expect a single one. 121 | if len(comment_tokens) != 1: 122 | msg = f"Expected there to be a single comment token, have {len(comment_tokens)}" 123 | raise ValueError( 124 | msg, 125 | ) 126 | 127 | comment_token = comment_tokens[0] 128 | python_code = line[0 : comment_token.start[1]] 129 | python_comment = line[comment_token.start[1] :] 130 | return python_code, python_comment 131 | 132 | 133 | def read_mypy_error_report( 134 | *, 135 | path_to_error_report: pathlib.Path, 136 | ) -> list[str]: 137 | error_report_lines = path_to_error_report.read_text().split("\n") 138 | # eg: "Found 1 error in 1 file (checked 5 source files)", have no use for this. 139 | summary_regex = re.compile(r"^Found [0-9]+ errors? in [0-9]+") 140 | error_report_lines_no_summary = [ 141 | line for line in error_report_lines if summary_regex.match(line) is None 142 | ] 143 | # typically a '' at the end of the report - any lines which are just '' (or ' ') are 144 | # of no use though. 145 | error_report_lines_filtered = [line for line in error_report_lines_no_summary if line.strip()] 146 | # return list sorted by file path (file path is at the start of all lines in error report). 147 | return sorted(error_report_lines_filtered) 148 | 149 | 150 | def update_files(*, file_updates: list[FileUpdate]) -> None: 151 | # update each line with `# type: ignore[]` 152 | for pth_and_line_num, grp in itertools.groupby( 153 | file_updates, 154 | key=lambda x: (x[0], x[1]), 155 | ): 156 | file_path, line_number = pth_and_line_num 157 | error_codes = ", ".join(x[2] for x in grp) 158 | file_lines = pathlib.Path(file_path).read_text(encoding="utf8").split("\n") 159 | 160 | python_code, python_comment = extract_code_comment(line=file_lines[line_number]) 161 | # In some cases it's possible for there to be multiple spaces added 162 | # before '# type: ...' whereas we'd like to ensure only two spaces are 163 | # added. 164 | python_code = python_code.rstrip() 165 | mypy_ignore = f"# type: ignore[{error_codes}]" 166 | 167 | if python_comment: 168 | line_update = f"{python_code} {mypy_ignore} {python_comment}" 169 | else: 170 | line_update = f"{python_code} {mypy_ignore}" 171 | 172 | # check to see if the line contains a trailing comment already - it it does then this line 173 | # needs to be handled separately. 174 | file_lines[line_number] = line_update.rstrip(" ") 175 | new_text = "\n".join(file_lines) 176 | with open(file_path, "w", encoding="utf8") as file: 177 | file.write(new_text) 178 | 179 | 180 | def line_contains_error(*, error_message: str) -> bool: 181 | """Ensure that the line contains an error message to extract.""" 182 | if re.match(r".*error.*\[.*\]$", error_message): 183 | return True 184 | return False 185 | 186 | 187 | def line_is_unused_ignore(*, error_message: str) -> bool: 188 | """ 189 | Return true if line relates to an unused ignore. 190 | 191 | These are treated differently to other messages, in this case the current 192 | type: ignore needs to be removed rather than adding one. 193 | """ 194 | if re.match('.*unused.ignore.*|Unused "type: ignore" comment', error_message): 195 | return True 196 | return False 197 | 198 | 199 | def extract_file_line_number_and_error_code( 200 | *, 201 | error_report_lines: list[str], 202 | ) -> list[FileUpdate]: 203 | file_updates: list[FileUpdate] = [] 204 | for error_line in error_report_lines: 205 | if (not line_contains_error(error_message=error_line)) or line_is_unused_ignore( 206 | error_message=error_line 207 | ): 208 | continue 209 | 210 | # Call to untyped function "main" in typed context [no-untyped-call]' 211 | file_path, line_number, *_ = error_line.split(":") 212 | # mypy will report the first line as '1' rather than '0'. 213 | line_num = int(line_number) - 1 214 | if error_message := re.match(r"^.*\[(.*)\]$", error_line): 215 | file_updates.append((file_path, line_num, error_message.group(1))) 216 | else: 217 | # haven't seen anything else yet, though there might be other error types which need to 218 | # be handled. 219 | msg = f"Unexpected line format: {error_line}" 220 | raise RuntimeError(msg) 221 | 222 | # Ensure that the returned updates are unique. For example we might have 223 | # file_updates as something like [('f.py', 0, 'attr-defined'), ('f.py', 0, 224 | # 'attr-defined')] given code such as object().foo, object().bar - leading 225 | # to igore[attr-defined, attr-defined] instead of ignore[attr-defined] 226 | return sorted(set(file_updates), key=lambda x: (x[0], x[1], x[2])) 227 | 228 | 229 | def add_type_ignores( 230 | *, 231 | report_output: pathlib.Path, 232 | ) -> None: 233 | """Add `# type: ignore` to all lines which fail on given mypy command.""" 234 | error_report_lines = read_mypy_error_report(path_to_error_report=report_output) 235 | exit_if_no_errors(report=error_report_lines) 236 | # process all lines in report. 237 | file_updates = extract_file_line_number_and_error_code( 238 | error_report_lines=error_report_lines, 239 | ) 240 | update_files(file_updates=file_updates) 241 | 242 | 243 | def remove_unused_ignores(*, report_output: str) -> str: 244 | """Remove ignores which are no longer needed, based on report output.""" 245 | report_lines = report_output.read_text().split("\n") 246 | ignores_lines: list[FileUpdate] = sorted( 247 | [ 248 | tuple(line.split(":", 2)) 249 | for line in report_lines 250 | if 'error: Unused "type: ignore" comment' in line 251 | ], 252 | key=lambda x: (x[0], x[1]), 253 | ) 254 | 255 | for file_path, grp in itertools.groupby(ignores_lines, key=lambda x: x[0]): 256 | _grp = sorted(grp) 257 | file_lines = pathlib.Path(file_path).read_text().split("\n") 258 | for _, line_n, _ in _grp: 259 | _line_n = int(line_n) - 1 # Decrease by 1 as mypy indexes from 1 not zero 260 | regexp = r"#\s*type:\s*ignore(?:\[[^\]]*\])?" 261 | file_lines[_line_n] = re.sub(regexp, "", file_lines[_line_n]).rstrip() 262 | 263 | # Write updated file out. 264 | with open(file_path, "w", encoding="utf8") as file: 265 | file.write("\n".join(file_lines)) 266 | 267 | 268 | # --- Call functions above. 269 | def create_parser() -> argparse.ArgumentParser: 270 | parser = argparse.ArgumentParser( 271 | description=textwrap.dedent( 272 | """ 273 | CLI tool for providing a clean slate for mypy usage within a project. 274 | 275 | Default expectation is to want to get a project into a state that it 276 | will pass mypy when run with `--strict`, if this isn't the case custom 277 | flags can be passed to mypy via the `--mypy_flags` argument. 278 | """ 279 | ).strip(), 280 | formatter_class=argparse.RawDescriptionHelpFormatter, 281 | # Hard-coding this as the usage is dynamic otherwise, based on where the 282 | # parser is defined. I'm using print_help() to generate the output of 283 | # --help into the README so need this to be consistent. Otherwise, 284 | # creating the parser from within mod.py will put mod.py into the usage 285 | # rather than the poetry.script entry point for the CLI. 286 | usage="mypy_clean_slate [options]", 287 | ) 288 | parser.add_argument( 289 | "-r", 290 | "--generate_mypy_error_report", 291 | help=("Generate 'mypy_error_report.txt' in the cwd."), 292 | action="store_true", 293 | ) 294 | 295 | parser.add_argument( 296 | "-p", 297 | "--path_to_code", 298 | help=("Where code is that needs report generating for it."), 299 | default=pathlib.Path("."), 300 | ) 301 | 302 | parser.add_argument( 303 | "-a", 304 | "--add_type_ignore", 305 | help=('Add "# type: ignore[]" to suppress all raised mypy errors.'), 306 | action="store_true", 307 | ) 308 | 309 | parser.add_argument( 310 | "--remove_unused", 311 | help=( 312 | 'Remove unused instances of "# type: ignore[]" ' 313 | "if raised as an error by mypy." 314 | ), 315 | action="store_true", 316 | default=False, 317 | ) 318 | 319 | parser.add_argument( 320 | "-o", 321 | "--mypy_report_output", 322 | help=("File to save report output to (default is mypy_error_report.txt)"), 323 | ) 324 | 325 | parser.add_argument( 326 | "--mypy_flags", 327 | type=str, 328 | default="", 329 | help=( 330 | "Custom flags to pass to mypy (provide them as a single string, " 331 | "default is to use --strict)" 332 | ), 333 | ) 334 | 335 | return parser 336 | 337 | 338 | def main() -> int: 339 | parser = create_parser() 340 | args = parser.parse_args() 341 | 342 | if args.mypy_report_output is None: 343 | report_output = pathlib.Path("mypy_error_report.txt") 344 | else: 345 | report_output = pathlib.Path(args.mypy_report_output) 346 | 347 | if args.generate_mypy_error_report: 348 | report = generate_mypy_error_report( 349 | path_to_code=args.path_to_code, 350 | mypy_flags=shlex.split(args.mypy_flags), 351 | ) 352 | report_output.write_text(report, encoding="utf8") 353 | 354 | if args.remove_unused: 355 | remove_unused_ignores(report_output=report_output) 356 | 357 | if args.add_type_ignore: 358 | add_type_ignores(report_output=report_output) 359 | 360 | return 0 361 | 362 | 363 | if __name__ == "__main__": 364 | sys.exit(main()) 365 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "asttokens" 5 | version = "2.4.1" 6 | description = "Annotate AST trees with source code positions" 7 | optional = false 8 | python-versions = "*" 9 | groups = ["dev"] 10 | files = [ 11 | {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, 12 | {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, 13 | ] 14 | 15 | [package.dependencies] 16 | six = ">=1.12.0" 17 | 18 | [package.extras] 19 | astroid = ["astroid (>=1,<2) ; python_version < \"3\"", "astroid (>=2,<4) ; python_version >= \"3\""] 20 | test = ["astroid (>=1,<2) ; python_version < \"3\"", "astroid (>=2,<4) ; python_version >= \"3\"", "pytest"] 21 | 22 | [[package]] 23 | name = "cfgv" 24 | version = "3.4.0" 25 | description = "Validate configuration and produce human readable error messages." 26 | optional = false 27 | python-versions = ">=3.8" 28 | groups = ["dev"] 29 | files = [ 30 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 31 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 32 | ] 33 | 34 | [[package]] 35 | name = "colorama" 36 | version = "0.4.6" 37 | description = "Cross-platform colored terminal text." 38 | optional = false 39 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 40 | groups = ["dev"] 41 | markers = "sys_platform == \"win32\"" 42 | files = [ 43 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 44 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 45 | ] 46 | 47 | [[package]] 48 | name = "coverage" 49 | version = "7.4.1" 50 | description = "Code coverage measurement for Python" 51 | optional = false 52 | python-versions = ">=3.8" 53 | groups = ["dev"] 54 | files = [ 55 | {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, 56 | {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, 57 | {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, 58 | {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, 59 | {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, 60 | {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, 61 | {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, 62 | {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, 63 | {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, 64 | {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, 65 | {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, 66 | {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, 67 | {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, 68 | {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, 69 | {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, 70 | {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, 71 | {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, 72 | {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, 73 | {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, 74 | {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, 75 | {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, 76 | {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, 77 | {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, 78 | {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, 79 | {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, 80 | {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, 81 | {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, 82 | {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, 83 | {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, 84 | {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, 85 | {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, 86 | {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, 87 | {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, 88 | {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, 89 | {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, 90 | {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, 91 | {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, 92 | {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, 93 | {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, 94 | {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, 95 | {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, 96 | {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, 97 | {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, 98 | {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, 99 | {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, 100 | {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, 101 | {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, 102 | {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, 103 | {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, 104 | {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, 105 | {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, 106 | {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, 107 | ] 108 | 109 | [package.dependencies] 110 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 111 | 112 | [package.extras] 113 | toml = ["tomli ; python_full_version <= \"3.11.0a6\""] 114 | 115 | [[package]] 116 | name = "decorator" 117 | version = "5.1.1" 118 | description = "Decorators for Humans" 119 | optional = false 120 | python-versions = ">=3.5" 121 | groups = ["dev"] 122 | files = [ 123 | {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, 124 | {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, 125 | ] 126 | 127 | [[package]] 128 | name = "distlib" 129 | version = "0.3.8" 130 | description = "Distribution utilities" 131 | optional = false 132 | python-versions = "*" 133 | groups = ["dev"] 134 | files = [ 135 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 136 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 137 | ] 138 | 139 | [[package]] 140 | name = "exceptiongroup" 141 | version = "1.2.0" 142 | description = "Backport of PEP 654 (exception groups)" 143 | optional = false 144 | python-versions = ">=3.7" 145 | groups = ["dev"] 146 | markers = "python_version < \"3.11\"" 147 | files = [ 148 | {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, 149 | {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, 150 | ] 151 | 152 | [package.extras] 153 | test = ["pytest (>=6)"] 154 | 155 | [[package]] 156 | name = "executing" 157 | version = "2.0.1" 158 | description = "Get the currently executing AST node of a frame, and other information" 159 | optional = false 160 | python-versions = ">=3.5" 161 | groups = ["dev"] 162 | files = [ 163 | {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, 164 | {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, 165 | ] 166 | 167 | [package.extras] 168 | tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] 169 | 170 | [[package]] 171 | name = "fancycompleter" 172 | version = "0.11.0" 173 | description = "colorful TAB completion for Python prompt" 174 | optional = false 175 | python-versions = ">=3.8" 176 | groups = ["dev"] 177 | files = [ 178 | {file = "fancycompleter-0.11.0-py3-none-any.whl", hash = "sha256:a4712fdda8d7f3df08511ab2755ea0f1e669e2c65701a28c0c0aa2ff528521ed"}, 179 | {file = "fancycompleter-0.11.0.tar.gz", hash = "sha256:632b265b29dd0315b96d33d13d83132a541d6312262214f50211b3981bb4fa00"}, 180 | ] 181 | 182 | [package.dependencies] 183 | pyreadline3 = {version = "*", markers = "platform_system == \"Windows\""} 184 | pyrepl = {version = ">=0.11.3", markers = "python_version < \"3.13\""} 185 | 186 | [package.extras] 187 | dev = ["fancycompleter[tests]", "mypy", "ruff (==0.11.2)"] 188 | tests = ["pytest", "pytest-cov"] 189 | 190 | [[package]] 191 | name = "filelock" 192 | version = "3.13.1" 193 | description = "A platform independent file lock." 194 | optional = false 195 | python-versions = ">=3.8" 196 | groups = ["dev"] 197 | files = [ 198 | {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, 199 | {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, 200 | ] 201 | 202 | [package.extras] 203 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] 204 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] 205 | typing = ["typing-extensions (>=4.8) ; python_version < \"3.11\""] 206 | 207 | [[package]] 208 | name = "identify" 209 | version = "2.5.34" 210 | description = "File identification library for Python" 211 | optional = false 212 | python-versions = ">=3.8" 213 | groups = ["dev"] 214 | files = [ 215 | {file = "identify-2.5.34-py2.py3-none-any.whl", hash = "sha256:a4316013779e433d08b96e5eabb7f641e6c7942e4ab5d4c509ebd2e7a8994aed"}, 216 | {file = "identify-2.5.34.tar.gz", hash = "sha256:ee17bc9d499899bc9eaec1ac7bf2dc9eedd480db9d88b96d123d3b64a9d34f5d"}, 217 | ] 218 | 219 | [package.extras] 220 | license = ["ukkonen"] 221 | 222 | [[package]] 223 | name = "iniconfig" 224 | version = "2.0.0" 225 | description = "brain-dead simple config-ini parsing" 226 | optional = false 227 | python-versions = ">=3.7" 228 | groups = ["dev"] 229 | files = [ 230 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 231 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 232 | ] 233 | 234 | [[package]] 235 | name = "ipdb" 236 | version = "0.13.13" 237 | description = "IPython-enabled pdb" 238 | optional = false 239 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 240 | groups = ["dev"] 241 | files = [ 242 | {file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"}, 243 | {file = "ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726"}, 244 | ] 245 | 246 | [package.dependencies] 247 | decorator = {version = "*", markers = "python_version > \"3.6\""} 248 | ipython = {version = ">=7.31.1", markers = "python_version > \"3.6\""} 249 | tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""} 250 | 251 | [[package]] 252 | name = "ipython" 253 | version = "8.18.1" 254 | description = "IPython: Productive Interactive Computing" 255 | optional = false 256 | python-versions = ">=3.9" 257 | groups = ["dev"] 258 | files = [ 259 | {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, 260 | {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, 261 | ] 262 | 263 | [package.dependencies] 264 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 265 | decorator = "*" 266 | exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} 267 | jedi = ">=0.16" 268 | matplotlib-inline = "*" 269 | pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} 270 | prompt-toolkit = ">=3.0.41,<3.1.0" 271 | pygments = ">=2.4.0" 272 | stack-data = "*" 273 | traitlets = ">=5" 274 | typing-extensions = {version = "*", markers = "python_version < \"3.10\""} 275 | 276 | [package.extras] 277 | all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] 278 | black = ["black"] 279 | doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] 280 | kernel = ["ipykernel"] 281 | nbconvert = ["nbconvert"] 282 | nbformat = ["nbformat"] 283 | notebook = ["ipywidgets", "notebook"] 284 | parallel = ["ipyparallel"] 285 | qtconsole = ["qtconsole"] 286 | test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] 287 | test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] 288 | 289 | [[package]] 290 | name = "jedi" 291 | version = "0.19.1" 292 | description = "An autocompletion tool for Python that can be used for text editors." 293 | optional = false 294 | python-versions = ">=3.6" 295 | groups = ["dev"] 296 | files = [ 297 | {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, 298 | {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, 299 | ] 300 | 301 | [package.dependencies] 302 | parso = ">=0.8.3,<0.9.0" 303 | 304 | [package.extras] 305 | docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] 306 | qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] 307 | testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] 308 | 309 | [[package]] 310 | name = "matplotlib-inline" 311 | version = "0.1.6" 312 | description = "Inline Matplotlib backend for Jupyter" 313 | optional = false 314 | python-versions = ">=3.5" 315 | groups = ["dev"] 316 | files = [ 317 | {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, 318 | {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, 319 | ] 320 | 321 | [package.dependencies] 322 | traitlets = "*" 323 | 324 | [[package]] 325 | name = "mypy" 326 | version = "1.16.0" 327 | description = "Optional static typing for Python" 328 | optional = false 329 | python-versions = ">=3.9" 330 | groups = ["main"] 331 | files = [ 332 | {file = "mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c"}, 333 | {file = "mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571"}, 334 | {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491"}, 335 | {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777"}, 336 | {file = "mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b"}, 337 | {file = "mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93"}, 338 | {file = "mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab"}, 339 | {file = "mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2"}, 340 | {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff"}, 341 | {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666"}, 342 | {file = "mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c"}, 343 | {file = "mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b"}, 344 | {file = "mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13"}, 345 | {file = "mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090"}, 346 | {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1"}, 347 | {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8"}, 348 | {file = "mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730"}, 349 | {file = "mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec"}, 350 | {file = "mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b"}, 351 | {file = "mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0"}, 352 | {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b"}, 353 | {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d"}, 354 | {file = "mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52"}, 355 | {file = "mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb"}, 356 | {file = "mypy-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f56236114c425620875c7cf71700e3d60004858da856c6fc78998ffe767b73d3"}, 357 | {file = "mypy-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:15486beea80be24ff067d7d0ede673b001d0d684d0095803b3e6e17a886a2a92"}, 358 | {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2ed0e0847a80655afa2c121835b848ed101cc7b8d8d6ecc5205aedc732b1436"}, 359 | {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb5fbc8063cb4fde7787e4c0406aa63094a34a2daf4673f359a1fb64050e9cb2"}, 360 | {file = "mypy-1.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a5fcfdb7318c6a8dd127b14b1052743b83e97a970f0edb6c913211507a255e20"}, 361 | {file = "mypy-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:2e7e0ad35275e02797323a5aa1be0b14a4d03ffdb2e5f2b0489fa07b89c67b21"}, 362 | {file = "mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031"}, 363 | {file = "mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab"}, 364 | ] 365 | 366 | [package.dependencies] 367 | mypy_extensions = ">=1.0.0" 368 | pathspec = ">=0.9.0" 369 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 370 | typing_extensions = ">=4.6.0" 371 | 372 | [package.extras] 373 | dmypy = ["psutil (>=4.0)"] 374 | faster-cache = ["orjson"] 375 | install-types = ["pip"] 376 | mypyc = ["setuptools (>=50)"] 377 | reports = ["lxml"] 378 | 379 | [[package]] 380 | name = "mypy-extensions" 381 | version = "1.0.0" 382 | description = "Type system extensions for programs checked with the mypy type checker." 383 | optional = false 384 | python-versions = ">=3.5" 385 | groups = ["main"] 386 | files = [ 387 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 388 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 389 | ] 390 | 391 | [[package]] 392 | name = "nodeenv" 393 | version = "1.8.0" 394 | description = "Node.js virtual environment builder" 395 | optional = false 396 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" 397 | groups = ["dev"] 398 | files = [ 399 | {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, 400 | {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, 401 | ] 402 | 403 | [package.dependencies] 404 | setuptools = "*" 405 | 406 | [[package]] 407 | name = "packaging" 408 | version = "23.2" 409 | description = "Core utilities for Python packages" 410 | optional = false 411 | python-versions = ">=3.7" 412 | groups = ["dev"] 413 | files = [ 414 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, 415 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, 416 | ] 417 | 418 | [[package]] 419 | name = "parso" 420 | version = "0.8.3" 421 | description = "A Python Parser" 422 | optional = false 423 | python-versions = ">=3.6" 424 | groups = ["dev"] 425 | files = [ 426 | {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, 427 | {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, 428 | ] 429 | 430 | [package.extras] 431 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 432 | testing = ["docopt", "pytest (<6.0.0)"] 433 | 434 | [[package]] 435 | name = "pathspec" 436 | version = "0.12.1" 437 | description = "Utility library for gitignore style pattern matching of file paths." 438 | optional = false 439 | python-versions = ">=3.8" 440 | groups = ["main"] 441 | files = [ 442 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 443 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 444 | ] 445 | 446 | [[package]] 447 | name = "pdbpp" 448 | version = "0.11.6" 449 | description = "pdb++, a drop-in replacement for pdb" 450 | optional = false 451 | python-versions = "*" 452 | groups = ["dev"] 453 | files = [ 454 | {file = "pdbpp-0.11.6-py3-none-any.whl", hash = "sha256:8e024d36bd2f35a3b19d8732524c696b8b4aef633250d28547198e746cd81ccb"}, 455 | {file = "pdbpp-0.11.6.tar.gz", hash = "sha256:36a73c5bcf0c3c35034be4cf99e6106e3ee0c8f5e0faafc2cf9be5f1481eb4b7"}, 456 | ] 457 | 458 | [package.dependencies] 459 | fancycompleter = ">=0.11.0" 460 | pygments = "*" 461 | 462 | [package.extras] 463 | testing = ["ipython", "pexpect", "pytest", "pytest-cov"] 464 | 465 | [[package]] 466 | name = "pexpect" 467 | version = "4.9.0" 468 | description = "Pexpect allows easy control of interactive console applications." 469 | optional = false 470 | python-versions = "*" 471 | groups = ["dev"] 472 | markers = "sys_platform != \"win32\"" 473 | files = [ 474 | {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, 475 | {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, 476 | ] 477 | 478 | [package.dependencies] 479 | ptyprocess = ">=0.5" 480 | 481 | [[package]] 482 | name = "platformdirs" 483 | version = "4.2.0" 484 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 485 | optional = false 486 | python-versions = ">=3.8" 487 | groups = ["dev"] 488 | files = [ 489 | {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, 490 | {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, 491 | ] 492 | 493 | [package.extras] 494 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 495 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 496 | 497 | [[package]] 498 | name = "pluggy" 499 | version = "1.5.0" 500 | description = "plugin and hook calling mechanisms for python" 501 | optional = false 502 | python-versions = ">=3.8" 503 | groups = ["dev"] 504 | files = [ 505 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 506 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 507 | ] 508 | 509 | [package.extras] 510 | dev = ["pre-commit", "tox"] 511 | testing = ["pytest", "pytest-benchmark"] 512 | 513 | [[package]] 514 | name = "pre-commit" 515 | version = "3.8.0" 516 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 517 | optional = false 518 | python-versions = ">=3.9" 519 | groups = ["dev"] 520 | files = [ 521 | {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, 522 | {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, 523 | ] 524 | 525 | [package.dependencies] 526 | cfgv = ">=2.0.0" 527 | identify = ">=1.0.0" 528 | nodeenv = ">=0.11.1" 529 | pyyaml = ">=5.1" 530 | virtualenv = ">=20.10.0" 531 | 532 | [[package]] 533 | name = "prompt-toolkit" 534 | version = "3.0.43" 535 | description = "Library for building powerful interactive command lines in Python" 536 | optional = false 537 | python-versions = ">=3.7.0" 538 | groups = ["dev"] 539 | files = [ 540 | {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, 541 | {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, 542 | ] 543 | 544 | [package.dependencies] 545 | wcwidth = "*" 546 | 547 | [[package]] 548 | name = "ptyprocess" 549 | version = "0.7.0" 550 | description = "Run a subprocess in a pseudo terminal" 551 | optional = false 552 | python-versions = "*" 553 | groups = ["dev"] 554 | markers = "sys_platform != \"win32\"" 555 | files = [ 556 | {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, 557 | {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, 558 | ] 559 | 560 | [[package]] 561 | name = "pure-eval" 562 | version = "0.2.2" 563 | description = "Safely evaluate AST nodes without side effects" 564 | optional = false 565 | python-versions = "*" 566 | groups = ["dev"] 567 | files = [ 568 | {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, 569 | {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, 570 | ] 571 | 572 | [package.extras] 573 | tests = ["pytest"] 574 | 575 | [[package]] 576 | name = "pygments" 577 | version = "2.17.2" 578 | description = "Pygments is a syntax highlighting package written in Python." 579 | optional = false 580 | python-versions = ">=3.7" 581 | groups = ["dev"] 582 | files = [ 583 | {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, 584 | {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, 585 | ] 586 | 587 | [package.extras] 588 | plugins = ["importlib-metadata ; python_version < \"3.8\""] 589 | windows-terminal = ["colorama (>=0.4.6)"] 590 | 591 | [[package]] 592 | name = "pyreadline3" 593 | version = "3.5.4" 594 | description = "A python implementation of GNU readline." 595 | optional = false 596 | python-versions = ">=3.8" 597 | groups = ["dev"] 598 | markers = "platform_system == \"Windows\"" 599 | files = [ 600 | {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, 601 | {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, 602 | ] 603 | 604 | [package.extras] 605 | dev = ["build", "flake8", "mypy", "pytest", "twine"] 606 | 607 | [[package]] 608 | name = "pyrepl" 609 | version = "0.11.3.post1" 610 | description = "A library for building flexible command line interfaces" 611 | optional = false 612 | python-versions = ">=3.8" 613 | groups = ["dev"] 614 | markers = "python_version < \"3.13\"" 615 | files = [ 616 | {file = "pyrepl-0.11.3.post1-py3-none-any.whl", hash = "sha256:59fcd67588892731dc6e7aff106c380d303d54324ff028827804a2b056223d92"}, 617 | {file = "pyrepl-0.11.3.post1.tar.gz", hash = "sha256:0ca7568c8be919b69f99644d29d31738a5b1a87750d06dd36564bcfad278d402"}, 618 | ] 619 | 620 | [package.extras] 621 | dev = ["pyrepl[tests]", "ruff (==0.9.3)"] 622 | tests = ["pexpect", "pytest", "pytest-coverage", "pytest-timeout"] 623 | 624 | [[package]] 625 | name = "pytest" 626 | version = "8.4.0" 627 | description = "pytest: simple powerful testing with Python" 628 | optional = false 629 | python-versions = ">=3.9" 630 | groups = ["dev"] 631 | files = [ 632 | {file = "pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"}, 633 | {file = "pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"}, 634 | ] 635 | 636 | [package.dependencies] 637 | colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} 638 | exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} 639 | iniconfig = ">=1" 640 | packaging = ">=20" 641 | pluggy = ">=1.5,<2" 642 | pygments = ">=2.7.2" 643 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 644 | 645 | [package.extras] 646 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] 647 | 648 | [[package]] 649 | name = "pytest-cov" 650 | version = "4.1.0" 651 | description = "Pytest plugin for measuring coverage." 652 | optional = false 653 | python-versions = ">=3.7" 654 | groups = ["dev"] 655 | files = [ 656 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 657 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 658 | ] 659 | 660 | [package.dependencies] 661 | coverage = {version = ">=5.2.1", extras = ["toml"]} 662 | pytest = ">=4.6" 663 | 664 | [package.extras] 665 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 666 | 667 | [[package]] 668 | name = "pyyaml" 669 | version = "6.0.1" 670 | description = "YAML parser and emitter for Python" 671 | optional = false 672 | python-versions = ">=3.6" 673 | groups = ["dev"] 674 | files = [ 675 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 676 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 677 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 678 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 679 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 680 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 681 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 682 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 683 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 684 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 685 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 686 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 687 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 688 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 689 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 690 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 691 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 692 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 693 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 694 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 695 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 696 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 697 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 698 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 699 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 700 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 701 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 702 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 703 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 704 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 705 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 706 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 707 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 708 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 709 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 710 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 711 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 712 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 713 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 714 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 715 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 716 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 717 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 718 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 719 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 720 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 721 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 722 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 723 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 724 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 725 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 726 | ] 727 | 728 | [[package]] 729 | name = "ruff" 730 | version = "0.11.12" 731 | description = "An extremely fast Python linter and code formatter, written in Rust." 732 | optional = false 733 | python-versions = ">=3.7" 734 | groups = ["dev"] 735 | files = [ 736 | {file = "ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc"}, 737 | {file = "ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3"}, 738 | {file = "ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa"}, 739 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012"}, 740 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a"}, 741 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7"}, 742 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a"}, 743 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13"}, 744 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be"}, 745 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd"}, 746 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef"}, 747 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5"}, 748 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02"}, 749 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c"}, 750 | {file = "ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6"}, 751 | {file = "ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832"}, 752 | {file = "ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5"}, 753 | {file = "ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603"}, 754 | ] 755 | 756 | [[package]] 757 | name = "setuptools" 758 | version = "69.1.0" 759 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 760 | optional = false 761 | python-versions = ">=3.8" 762 | groups = ["dev"] 763 | files = [ 764 | {file = "setuptools-69.1.0-py3-none-any.whl", hash = "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6"}, 765 | {file = "setuptools-69.1.0.tar.gz", hash = "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401"}, 766 | ] 767 | 768 | [package.extras] 769 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 770 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov ; platform_python_implementation != \"PyPy\"", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 771 | testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 772 | 773 | [[package]] 774 | name = "six" 775 | version = "1.16.0" 776 | description = "Python 2 and 3 compatibility utilities" 777 | optional = false 778 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 779 | groups = ["dev"] 780 | files = [ 781 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 782 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 783 | ] 784 | 785 | [[package]] 786 | name = "stack-data" 787 | version = "0.6.3" 788 | description = "Extract data from python stack frames and tracebacks for informative displays" 789 | optional = false 790 | python-versions = "*" 791 | groups = ["dev"] 792 | files = [ 793 | {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, 794 | {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, 795 | ] 796 | 797 | [package.dependencies] 798 | asttokens = ">=2.1.0" 799 | executing = ">=1.2.0" 800 | pure-eval = "*" 801 | 802 | [package.extras] 803 | tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] 804 | 805 | [[package]] 806 | name = "tomli" 807 | version = "2.0.1" 808 | description = "A lil' TOML parser" 809 | optional = false 810 | python-versions = ">=3.7" 811 | groups = ["main", "dev"] 812 | files = [ 813 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 814 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 815 | ] 816 | markers = {main = "python_version < \"3.11\"", dev = "python_full_version <= \"3.11.0a6\""} 817 | 818 | [[package]] 819 | name = "traitlets" 820 | version = "5.14.1" 821 | description = "Traitlets Python configuration system" 822 | optional = false 823 | python-versions = ">=3.8" 824 | groups = ["dev"] 825 | files = [ 826 | {file = "traitlets-5.14.1-py3-none-any.whl", hash = "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74"}, 827 | {file = "traitlets-5.14.1.tar.gz", hash = "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"}, 828 | ] 829 | 830 | [package.extras] 831 | docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] 832 | test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] 833 | 834 | [[package]] 835 | name = "typing-extensions" 836 | version = "4.9.0" 837 | description = "Backported and Experimental Type Hints for Python 3.8+" 838 | optional = false 839 | python-versions = ">=3.8" 840 | groups = ["main", "dev"] 841 | files = [ 842 | {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, 843 | {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, 844 | ] 845 | markers = {dev = "python_version < \"3.10\""} 846 | 847 | [[package]] 848 | name = "virtualenv" 849 | version = "20.25.0" 850 | description = "Virtual Python Environment builder" 851 | optional = false 852 | python-versions = ">=3.7" 853 | groups = ["dev"] 854 | files = [ 855 | {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, 856 | {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, 857 | ] 858 | 859 | [package.dependencies] 860 | distlib = ">=0.3.7,<1" 861 | filelock = ">=3.12.2,<4" 862 | platformdirs = ">=3.9.1,<5" 863 | 864 | [package.extras] 865 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 866 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] 867 | 868 | [[package]] 869 | name = "wcwidth" 870 | version = "0.2.13" 871 | description = "Measures the displayed width of unicode strings in a terminal" 872 | optional = false 873 | python-versions = "*" 874 | groups = ["dev"] 875 | files = [ 876 | {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, 877 | {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, 878 | ] 879 | 880 | [metadata] 881 | lock-version = "2.1" 882 | python-versions = "^3.9" 883 | content-hash = "50fdd971c498d3becd86cc3c12726def0e0e56cf1be380428717f082dbb5a33b" 884 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "mypy_clean_slate" 3 | version = "0.3.4" 4 | description = "CLI tool for providing a clean slate for mypy usage within a project." 5 | authors = ["George Lenton "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/geo7/mypy_clean_slate" 9 | keywords = ['mypy', 'typing', 'typehint', 'type-hint'] 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.9" 13 | mypy = ">=0.910,<2.0" 14 | 15 | [tool.poetry.group.dev.dependencies] 16 | pytest = ">=8.0.0,<9.0.0" 17 | pre-commit = ">=2.17,<4.0" 18 | ipython = ">=8.18.1" 19 | ipdb = "^0.13.9" 20 | ruff = ">=0.0.265" 21 | pytest-cov = "^4.1.0" 22 | pdbpp = ">=0.10.3,<0.12.0" 23 | 24 | [tool.poetry.build] 25 | generate-setup-file = false 26 | 27 | [tool.poetry.scripts] 28 | mypy_clean_slate = "mypy_clean_slate.main:main" 29 | 30 | [build-system] 31 | requires = ["poetry-core>=1.0.0"] 32 | build-backend = "poetry.core.masonry.api" 33 | 34 | [tool.ruff] 35 | line-length = 100 36 | 37 | [tool.ruff.lint] 38 | select = ["ALL"] 39 | ignore = [ 40 | "ANN101", # Missing type annotation for `self` in method 41 | "ANN102", # Missing type annotation for `cls` in classmethod 42 | "ANN204", # Missing return type annotation for special method `__attrs_post_init__` 43 | "ANN206", # Missing return type annotation for classmethod `get_all_human_readable` 44 | "D100", # Missing docstring in public module 45 | "COM812", # missing-trailing-comma 46 | "D101", # Missing docstring in public class 47 | "D102", # Missing docstring in public method 48 | "D103", # Missing docstring in public function 49 | "D104", # Missing docstring in public package 50 | "D105", # Missing docstring in magic method 51 | "D107", # Missing docstring in `__init__` 52 | "D211", # no-blank-line-before-class 53 | "D212", # multi-line-summary-first-line 54 | "D401", # First line of docstring should be in imperative mood. 55 | "FIX002", # Line contains TODO, consider resolving the issue 56 | "ISC001", # single-line-implicit-string-concatenation 57 | "PD901", # `df` is a bad variable name. 58 | "PLR0913", # Too many arguments to function call (6 > 5) 59 | "PLW1510", # `subprocess.run` without explicit `check` argument 60 | "PTH123", # `open()` should be replaced by `Path.open()` 61 | "PTH201", # [*] Do not pass the current directory explicitly to `Path` 62 | "RUF010", # [*] Use explicit conversion flag 63 | "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes 64 | "S603", # `subprocess` call: check for execution of untrusted input 65 | "T100", # Trace found: `breakpoint` used 66 | "T201", # `print` found 67 | ] 68 | fixable = ["ALL"] 69 | unfixable = [] 70 | 71 | [tool.ruff.lint.per-file-ignores] 72 | "__init__.py" = ["F401"] 73 | "scratch/*" = ["ALL"] 74 | "tests/*" = [ 75 | "ANN001", # Missing type annotation for function argument `tmpdir` 76 | "ANN201", # Missing return type annotation for public function `test_get_file_types` 77 | "D100", # Missing docstring in public module 78 | "D101", # Missing docstring in public class 79 | "D102", # Missing docstring in public method 80 | "S101", # Use of `assert` detected 81 | ] 82 | 83 | [tool.ruff.lint.isort] 84 | section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] 85 | case-sensitive = true 86 | combine-as-imports = true 87 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geo7/mypy_clean_slate/03209cb829177a292453972f4ad74d2192eda0ca/scripts/__init__.py -------------------------------------------------------------------------------- /scripts/add_help_to_readme.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add --help output from cli to README usage section. 3 | 4 | Little script to make it easier to keep the README Usage section up to date with 5 | the parsers help output. 6 | """ 7 | 8 | from io import StringIO 9 | from pathlib import Path 10 | 11 | from mypy_clean_slate.main import create_parser 12 | 13 | 14 | def cli_help_text() -> str: 15 | """Get --help from argparse parser.""" 16 | parser = create_parser() 17 | cli_help = StringIO() 18 | parser.print_help(file=cli_help) 19 | return cli_help.getvalue() 20 | 21 | 22 | def update_readme_cli_help() -> str: 23 | """Generate README with updated cli --help.""" 24 | result = cli_help_text() 25 | readme = Path("./README.md").read_text() 26 | split_string = "[comment]: # (CLI help split)\n" 27 | splits = readme.split(split_string) 28 | return splits[0] + split_string + "\n```\n" + result + "\n```\n\n" + split_string + splits[2] 29 | 30 | 31 | def main() -> int: 32 | updated_readme = update_readme_cli_help() 33 | with open("README.md", "w") as f: 34 | f.write(updated_readme) 35 | return 0 36 | 37 | 38 | if __name__ == "__main__": 39 | raise SystemExit(main()) 40 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | eradicate-aggressive = False 3 | max-line-length = 89 4 | extend-ignore = 5 | D1, # don't enforce docstrings, but check if they exist. 6 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geo7/mypy_clean_slate/03209cb829177a292453972f4ad74d2192eda0ca/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_generate_readme.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pathlib 4 | import sys 5 | 6 | import pytest 7 | 8 | from scripts import add_help_to_readme 9 | 10 | 11 | @pytest.mark.skipif( 12 | # This test isn't critical to any application logic, and is caught by 13 | # testing on other versions in CI. Change here is that argparse from 14 | # >3.9 uses 'options' rather than 'optional arguments'. 15 | sys.version_info < (3, 10), 16 | reason="Changes in argparse output from 3.9 onwards.", 17 | ) 18 | def test_readme_cli_help() -> None: 19 | """Test the README has up to date help output.""" 20 | 21 | # For some reason I was getting some whitespace differences when generating 22 | # in different places, not sure why this was (the content was the same 23 | # otherwise) so am just comparing without \n or ' ' 24 | def strp(s: str) -> str: 25 | return s.replace("\n", "").replace(" ", "") 26 | 27 | updated = strp(add_help_to_readme.update_readme_cli_help()) 28 | existing = strp(pathlib.Path("README.md").read_text()) 29 | assert updated == existing 30 | -------------------------------------------------------------------------------- /tests/test_mypy_clean_slate.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pathlib 4 | import textwrap 5 | 6 | from mypy_clean_slate import __version__, main 7 | 8 | 9 | def test_version() -> None: 10 | # Ensure toml version is in sync with package version. 11 | with open("pyproject.toml") as f: 12 | pyproject_version = [line for line in f.readlines() if line.startswith("version = ")] 13 | assert len(pyproject_version) == 1 14 | assert pyproject_version[0].strip().split(" = ")[-1].replace('"', "") == __version__ 15 | 16 | 17 | def test_mypy_clean_slate_usage(tmp_path: pathlib.Path) -> None: 18 | # atm this is a pretty broad usage test - just checks that things are, pretty much, 19 | # working as expected. 20 | py_file_before_fix = textwrap.dedent( 21 | """ 22 | from __future__ import annotations 23 | 24 | 25 | def add(*, arg_1, arg_2): 26 | return arg_1 + arg_2 27 | 28 | 29 | add(arg_1=1, arg_2="s") # inline comment. 30 | 31 | 32 | def useless_sub(*, arg_1: float, arg_2: Sequence): 33 | return add(arg_1=arg_1, arg_2="what") - arg_2 34 | 35 | 36 | useless_sub(arg_1=3, arg_2=4) 37 | useless_sub(arg_1=3, arg_2="4") 38 | """, 39 | ).strip() 40 | 41 | py_file_after_fix = textwrap.dedent( 42 | """ 43 | from __future__ import annotations 44 | 45 | 46 | def add(*, arg_1, arg_2): # type: ignore[no-untyped-def] 47 | return arg_1 + arg_2 48 | 49 | 50 | add(arg_1=1, arg_2="s") # type: ignore[no-untyped-call] # inline comment. 51 | 52 | 53 | def useless_sub(*, arg_1: float, arg_2: Sequence): # type: ignore[name-defined, no-untyped-def] 54 | return add(arg_1=arg_1, arg_2="what") - arg_2 # type: ignore[no-untyped-call] 55 | 56 | 57 | useless_sub(arg_1=3, arg_2=4) 58 | useless_sub(arg_1=3, arg_2="4") 59 | """.strip(), 60 | ) 61 | 62 | python_file = pathlib.Path(tmp_path, "file_to_check.py") 63 | python_file.write_text(py_file_before_fix, encoding="utf8") 64 | 65 | # there's probably a much nicer way to write these tests. 66 | report_output = pathlib.Path(tmp_path, "testing_report_output.txt") 67 | report_output.write_text( 68 | main.generate_mypy_error_report(path_to_code=python_file, mypy_flags=[""]), 69 | encoding="utf8", 70 | ) 71 | 72 | main.add_type_ignores(report_output=report_output) 73 | assert python_file.read_text(encoding="utf8").strip() == py_file_after_fix 74 | 75 | 76 | def test_no_duplicate_codes_added(tmp_path: pathlib.Path) -> None: 77 | """Ensure duplicate ignore messages aren't applied.""" 78 | py_file_before_fix = textwrap.dedent( 79 | """ 80 | from __future__ import annotations 81 | 82 | object().foo, object().bar 83 | """, 84 | ).strip() 85 | 86 | py_file_after_fix = textwrap.dedent( 87 | """ 88 | from __future__ import annotations 89 | 90 | object().foo, object().bar # type: ignore[attr-defined] 91 | """ 92 | ).strip() 93 | 94 | python_file = pathlib.Path(tmp_path, "file_to_check.py") 95 | python_file.write_text(py_file_before_fix, encoding="utf8") 96 | 97 | report_output = pathlib.Path(tmp_path, "testing_report_output.txt") 98 | report_output.write_text( 99 | main.generate_mypy_error_report(path_to_code=python_file, mypy_flags=[""]), 100 | encoding="utf8", 101 | ) 102 | 103 | main.add_type_ignores(report_output=report_output) 104 | assert python_file.read_text(encoding="utf8").strip() == py_file_after_fix 105 | 106 | 107 | def test_custom_mypy_flags(tmp_path: pathlib.Path) -> None: 108 | """Ensure custom mypy flags are respected.""" 109 | py_file_before_fix = textwrap.dedent( 110 | """ 111 | def f(x): 112 | return x ** 2 113 | 114 | def main() -> int: 115 | y = f(12) 116 | return 0 117 | 118 | if __name__ == '__main__': 119 | raise SystemExit(main()) 120 | """, 121 | ).strip() 122 | 123 | py_file_after_fix = textwrap.dedent( 124 | """ 125 | def f(x): 126 | return x ** 2 127 | 128 | def main() -> int: 129 | y = f(12) # type: ignore[no-untyped-call] 130 | return 0 131 | 132 | if __name__ == '__main__': 133 | raise SystemExit(main()) 134 | """ 135 | ).strip() 136 | 137 | python_file = pathlib.Path(tmp_path, "file_to_check.py") 138 | python_file.write_text(py_file_before_fix, encoding="utf8") 139 | 140 | # there's probably a much nicer way to write these tests. 141 | report_output = pathlib.Path(tmp_path, "testing_report_output.txt") 142 | report_output.write_text( 143 | main.generate_mypy_error_report( 144 | path_to_code=python_file, mypy_flags=["--disallow-untyped-calls"] 145 | ), 146 | encoding="utf8", 147 | ) 148 | 149 | main.add_type_ignores(report_output=report_output) 150 | assert python_file.read_text(encoding="utf8").strip() == py_file_after_fix 151 | 152 | 153 | def test_remove_used_ignores(tmp_path: pathlib.Path) -> None: 154 | """Ensure unused ignores raised as errors are removed.""" 155 | py_file_before_fix = textwrap.dedent( 156 | """ 157 | def f(x : float) -> float: 158 | return x ** 2 159 | 160 | def main() -> int: 161 | y = f(12) # type: ignore[no-untyped-call] 162 | return 0 163 | 164 | if __name__ == '__main__': 165 | raise SystemExit(main()) 166 | """, 167 | ).strip() 168 | 169 | py_file_after_fix = textwrap.dedent( 170 | """ 171 | def f(x : float) -> float: 172 | return x ** 2 173 | 174 | def main() -> int: 175 | y = f(12) 176 | return 0 177 | 178 | if __name__ == '__main__': 179 | raise SystemExit(main()) 180 | """ 181 | ).strip() 182 | 183 | python_file = pathlib.Path(tmp_path, "file_to_check.py") 184 | python_file.write_text(py_file_before_fix, encoding="utf8") 185 | 186 | # there's probably a much nicer way to write these tests. 187 | report_output = pathlib.Path(tmp_path, "testing_report_output.txt") 188 | report_output.write_text( 189 | main.generate_mypy_error_report( 190 | path_to_code=python_file, 191 | mypy_flags=[""], 192 | ), 193 | encoding="utf8", 194 | ) 195 | main.remove_unused_ignores(report_output=report_output) 196 | main.add_type_ignores(report_output=report_output) 197 | assert python_file.read_text(encoding="utf8").strip() == py_file_after_fix 198 | --------------------------------------------------------------------------------