├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── .gitignore ├── .pre-commit-hooks.yaml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── poetry.lock ├── poetry.toml ├── pybetter ├── __init__.py ├── __main__.py ├── cli.py ├── improvements.py ├── transformers │ ├── __init__.py │ ├── all_attribute.py │ ├── base.py │ ├── boolean_equality.py │ ├── empty_fstring.py │ ├── equals_none.py │ ├── mutable_args.py │ ├── nested_withs.py │ ├── not_in.py │ ├── not_is.py │ ├── parenthesized_return.py │ └── unhashable_list.py └── utils.py ├── pyproject.toml ├── pytest.ini ├── test.py └── tests ├── __init__.py ├── conftest.py ├── test_all_attribute.py ├── test_boolean_equality.py ├── test_equals_none.py ├── test_mutable_args.py ├── test_nested_withs.py ├── test_no_crashes.py ├── test_not_in.py ├── test_not_is.py ├── test_parenthesized_return.py ├── test_trivial_fstring.py └── test_unhashable_list.py /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | .gitignore 4 | 5 | # CI 6 | .codeclimate.yml 7 | .travis.yml 8 | .taskcluster.yml 9 | 10 | # Docker 11 | docker-compose.yml 12 | .docker 13 | 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | */__pycache__/ 17 | */*/__pycache__/ 18 | */*/*/__pycache__/ 19 | *.py[cod] 20 | */*.py[cod] 21 | */*/*.py[cod] 22 | */*/*/*.py[cod] 23 | 24 | # C extensions 25 | *.so 26 | 27 | # Distribution / packaging 28 | .Python 29 | env/ 30 | build/ 31 | develop-eggs/ 32 | dist/ 33 | downloads/ 34 | eggs/ 35 | lib/ 36 | lib64/ 37 | parts/ 38 | sdist/ 39 | var/ 40 | *.egg-info/ 41 | .installed.cfg 42 | *.egg 43 | 44 | # PyInstaller 45 | # Usually these files are written by a python script from a template 46 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 47 | *.manifest 48 | *.spec 49 | 50 | # Installer logs 51 | pip-log.txt 52 | pip-delete-this-directory.txt 53 | 54 | # Unit test / coverage reports 55 | htmlcov/ 56 | .tox/ 57 | .coverage 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Virtual environment 76 | .env/ 77 | .venv/ 78 | venv/ 79 | 80 | # PyCharm 81 | .idea 82 | 83 | # Python mode for VIM 84 | .ropeproject 85 | */.ropeproject 86 | */*/.ropeproject 87 | */*/*/.ropeproject 88 | 89 | # Vim swap files 90 | *.swp 91 | */*.swp 92 | */*/*.swp 93 | */*/*/*.swp 94 | 95 | # Various tool caches 96 | .pyre 97 | .pytest_cache 98 | .hypothesis 99 | .ruff_cache 100 | .mypy_cache -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: flake8 11 | versions: 12 | - 3.9.0 13 | - dependency-name: ipdb 14 | versions: 15 | - 0.13.5 16 | - 0.13.6 17 | - dependency-name: pygments 18 | versions: 19 | - 2.8.0 20 | - dependency-name: pytest 21 | versions: 22 | - 6.2.2 23 | - dependency-name: libcst 24 | versions: 25 | - 0.3.17 26 | - dependency-name: mypy 27 | versions: 28 | - "0.800" 29 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | run-name: ${{ github.actor }} checks main branch for defects 3 | on: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | python: ["3.7", "3.8", "3.9", "3.10", "3.11"] 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out repository code 15 | uses: actions/checkout@v3 16 | 17 | - name: Install Python ${{ matrix.python }} 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python }} 21 | 22 | # Taken from https://jacobian.org/til/github-actions-poetry/ 23 | - name: Cache poetry install 24 | uses: actions/cache@v2 25 | with: 26 | path: ~/.local 27 | key: poetry-1.1.12-0 28 | 29 | - uses: snok/install-poetry@v1 30 | name: Install Poetry 31 | with: 32 | version: 1.2.1 33 | virtualenvs-create: true 34 | virtualenvs-in-project: true 35 | 36 | - name: Cache virtualenv for future runs 37 | id: cache-deps 38 | uses: actions/cache@v2 39 | with: 40 | path: .venv 41 | key: pydeps-${{ matrix.python }}-${{ hashFiles('poetry.lock') }} 42 | 43 | - run: poetry install --no-interaction --no-root 44 | name: Install dependencies via Poetry 45 | if: steps.cache-deps.outputs.cache-hit != 'true' 46 | 47 | - run: poetry install --no-interaction 48 | name: Install project 49 | 50 | - run: poetry run pytest 51 | name: Run tests 52 | 53 | - run: poetry add --group dev --no-interaction codecov 54 | name: Install codecov.io client 55 | 56 | - run: poetry run codecov 57 | name: Upload coverage data to codecov.io 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .idea/ 131 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: pybetter 2 | name: pybetter 3 | entry: pybetter 4 | language: python 5 | types: [python] 6 | require_serial: false 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version history 2 | 3 | 4 | ## 0.4.1 5 | 6 | ### Misc 7 | * Updated dependencies. 8 | * Now we use [LibCST](https://github.com/Instagram/LibCST) 0.4.1, which means that you should be able to run `pybetter` on all the Python 3 versions up to **3.11**! 9 | * Please be kind to each other and hug your loved ones. 10 | 11 | ## 0.4.0 12 | 13 | ### Features: 14 | 15 | * (B010) Added new fixer for `not A is B => A is not B` situation. (kudos to @rs2) 16 | 17 | ### Bugfixes: 18 | 19 | * (B003) Prevent removal of parentheses around empty tuple (kudos to @rs2) 20 | * (B008) Invalid translation with async context manager block (kudos to @lummax) 21 | 22 | ## 0.3.7 23 | 24 | ### Bugfixes: 25 | 26 | * (B004) Fix `typing.overload` annotations causing appearance of duplicate identifiers in `__all__` attribute (thanks, [Bernát Gábor](https://github.com/gaborbernat)!). 27 | 28 | ### Misc 29 | 30 | * Added [support](https://github.com/lensvol/pybetter/blob/master/.pre-commit-hooks.yaml) for `pre-commit` hooks (thanks, [Pavel Kedrinskiy](https://github.com/pavelpy)!). 31 | * Updated version of LibCST used from **0.3.16** to **0.3.19**. 32 | 33 | ## 0.3.6.1 34 | 35 | ### Misc 36 | 37 | * Fix another edge case with transforming "raw" f-strings (thanks, Zac Hatfield-Dodds!) 38 | 39 | 40 | ## 0.3.6 41 | 42 | ### Misc 43 | 44 | * Fix triple quotes being mangled by 'trivial f-string' transform (thanks, Zac Hatfield-Dodds!) 45 | * Updated dependencies. 46 | 47 | 48 | ## 0.3.5 49 | 50 | ### Misc: 51 | 52 | * Add `--exit-code` option to facilitate use as a static analysis tool. 53 | 54 | 55 | 56 | ## 0.3.4 57 | 58 | ### Bugfixes: 59 | 60 | * (B001) Fix issue with trying to convert multiple consecutive comparisons. 61 | * (B002) Properly process nested functions. 62 | * (B002) Ensure that order of arguments is preserved during iteration. 63 | * (B003) Fix unnecessary elimination of parens inside returned expression. 64 | 65 | ### Misc: 66 | 67 | * `--diff` option behaviour has changed: 68 | * Output is now being printed on `sys.stderr` instead if `sys.stdout`. 69 | * Source lines are no longer highlighted if `sys.stderr` is redirected away from TTY. 70 | 71 | 72 | 73 | ## 0.3.3 74 | 75 | ### Bugfixes: 76 | 77 | * Fix issue with `noqa` directive with one argument being treated as `noqa` without any arguments. 78 | * (B001) Fix arrangement of parentheses on transformed comparisons. 79 | 80 | ### Misc: 81 | 82 | * Use `pygments` for highlighting of diffs. 83 | * Output time taken to apply selected transformations. 84 | * Output total time taken to process all files in provided paths. 85 | * Now we use Travis CI to run tests on each commit. 86 | 87 | ### Features: 88 | 89 | * New "improvements" added: 90 | * **B008: Replace nested 'with' statements with a compound one.** 91 | * **B009: Replace unhashable list literals in set constructors** 92 | 93 | ## 0.3.2 94 | 95 | ### Bugfixes: 96 | 97 | * (B002) Fix issue when new inits were added before docstrings. 98 | * (B002) Remove unneeded indent after generated initializations. 99 | 100 | ### Misc: 101 | 102 | * Nothing is displayed if no changes were made to the file. 103 | 104 | 105 | 106 | ## 0.3.1 107 | 108 | ### Bugfixes: 109 | 110 | * (B003) No longer remove parentheses from multi-line return statements. 111 | 112 | * (B004) `a == False` is now correctly reduced to `not a`. 113 | 114 | 115 | 116 | ## 0.3.0 117 | 118 | ### Features: 119 | 120 | * Now we will recurse over every path provided and process all `*.py` files found. 121 | * Added support for `--select`/`--ignore` options for fine-tuning set of checks being run. 122 | * Added support for per-line disabling of specific checks using `noqa` comments. 123 | 124 | ### Bugfixes: 125 | 126 | * Variable names and constants are now properly added to `__all__` attribute. 127 | 128 | * Small fixes in README. 129 | 130 | 131 | 132 | ## 0.2.1 133 | 134 | ### Features: 135 | 136 | * New "improvements" added: 137 | * **B007: Convert f-strings without expressions into regular strings.** 138 | 139 | ### Misc: 140 | 141 | * Fix some typing violations. 142 | 143 | 144 | 145 | ## 0.2.0 146 | 147 | ### Bugfixes: 148 | 149 | * Now metadata resolution is done properly for all transformers. 150 | * Matcher decorators were used in a wrong way and caused a lot of false positives. 151 | * Now contents of the generated `__all__` attribute are lexicographically sorted. 152 | * Some transformers made changes on the `original_node` attribute, which could have led to subtle bugs. 153 | 154 | ### Features: 155 | 156 | * New "improvements" added: 157 | * **B005: Replace "A == None" with "A is None"** 158 | * **B006: Remove comparisons with either 'False' or 'True'.** 159 | 160 | * Now you can output diff between original and modified code using `--diff` option. 161 | 162 | 163 | 164 | ## 0.1.0 165 | 166 | - Initial release. 167 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Based on: 2 | # https://medium.com/@albertazzir/blazing-fast-python-docker-builds-with-poetry-a78a66f5aed0 3 | 4 | FROM python:3.11-buster as build 5 | 6 | RUN pip install poetry==1.6.1 7 | 8 | ENV POETRY_NO_INTERACTION=1 9 | ENV POETRY_VIRTUALENVS_IN_PROJECT=1 10 | ENV POETRY_VIRTUALENVS_CREATE=1 11 | ENV POETRY_CACHE_DIR='/tmp/poetry_cache' 12 | 13 | WORKDIR /app 14 | 15 | COPY poetry.lock pyproject.toml README.md ./ 16 | 17 | RUN poetry install --no-root --without dev && rm -rf $POETRY_CACHE_DIR 18 | 19 | FROM python:3.11-slim-buster as runtime 20 | 21 | ENV VIRTUAL_ENV=/app/.venv 22 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 23 | ENV PYTHONUNBUFFERED=1 24 | 25 | COPY --from=build $VIRTUAL_ENV $VIRTUAL_ENV 26 | 27 | COPY pybetter pybetter 28 | 29 | WORKDIR /src 30 | 31 | ADD . . 32 | 33 | ENTRYPOINT ["python3", "-m", "pybetter"] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2019 Google, Inc. http://angularjs.org 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 13 | all 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 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pybetter 2 | [![PyPI](https://img.shields.io/pypi/v/pybetter)](https://pypi.org/project/pybetter/) 3 | ![Downloads](https://img.shields.io/pypi/dm/pybetter) 4 | ![Build status](https://github.com/lensvol/pybetter/actions/workflows/tests.yml/badge.svg) 5 | ![Code coverage](https://img.shields.io/codecov/c/github/lensvol/pybetter) 6 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pybetter) 7 | [![License](https://img.shields.io/github/license/lensvol/pybetter)](LICENSE) 8 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 9 | 10 | Tool for fixing trivial problems with your code. 11 | 12 | Originally intended as an example for my PyCon Belarus 2020 talk about [LibCST](https://github.com/Instagram/LibCST). 13 | 14 | ## Usage 15 | 16 | Simply provide a valid Python source code file as one of the argument and it will try to fix any issues it could find. 17 | 18 | ``` 19 | Usage: pybetter [OPTIONS] [PATHS]... 20 | 21 | Options: 22 | --noop Do not make any changes to the source files. 23 | --diff Show diff-like output of the changes made. 24 | --select CODES Apply only improvements with the provided codes. 25 | --exclude CODES Exclude improvements with the provided codes. 26 | --exit-code Exit with provided code if fixes were applied. 27 | --help Show this message and exit. 28 | ``` 29 | 30 | 31 | 32 | ## Example 33 | 34 | ```shell 35 | # cat test.py 36 | def f(): 37 | return (42, "Hello, world") 38 | 39 | # pybetter test.py 40 | --> Processing 'test.py'... 41 | [+] (B003) Remove parentheses from the tuple in 'return' statement. 42 | All done! 43 | 44 | # cat test.py 45 | def f(): 46 | return 42, "Hello, world" 47 | 48 | ``` 49 | 50 | 51 | 52 | ## Available fixers 53 | 54 | * **B001: Replace 'not A in B' with 'A not in B'** 55 | 56 | Usage of `A not in B` over `not A in B` is recommended both by Google and [PEP-8](https://www.python.org/dev/peps/pep-0008/#programming-recommendations). Both of those forms are compiled to the same bytecode, but second form has some potential of confusion for the reader. 57 | 58 | ```python 59 | # BEFORE: 60 | if not 42 in counts: 61 | sys.exit(-1) 62 | 63 | # AFTER: 64 | if 42 not in counts: 65 | sys.exit(-1) 66 | ``` 67 | 68 | 69 | 70 | * **B002: Default values for `kwargs` are mutable.** 71 | 72 | As described in [Common Gotchas](https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments) section of "The Hitchhiker's Guide to Python", mutable arguments can be a tricky thing. This fixer replaces any default values that happen to be lists or dicts with **None** value, moving initialization from function definition into function body. 73 | 74 | ```python 75 | # BEFORE 76 | def p(a=[]): 77 | print(a) 78 | 79 | # AFTER 80 | def p(a=None): 81 | if a is None: 82 | a = [] 83 | 84 | print(a) 85 | ``` 86 | 87 | Be warned, that this fix may break code which *intentionally* uses mutable default arguments (e.g. caching). 88 | 89 | * **B003: Remove parentheses from the tuple in 'return' statement.** 90 | 91 | If you are returning a tuple from the function by implicitly constructing it, then additional parentheses around it are redundant. 92 | 93 | ```python 94 | # BEFORE: 95 | def hello(): 96 | return ("World", 42) 97 | 98 | # AFTER: 99 | def hello(): 100 | return "World", 42 101 | ``` 102 | 103 | * **B004: `__all__` attribute is missing.** 104 | 105 | Regenerate missing `__all__` attribute, filling it with the list of top-level function and class names. 106 | 107 | *NB*: It will ignore any names starting with `_` to prevent any private members from ending up in the list. 108 | 109 | ```python 110 | # BEFORE: 111 | def hello(): 112 | return ("World", 42) 113 | 114 | class F: 115 | pass 116 | 117 | # AFTER: 118 | def hello(): 119 | return "World", 42 120 | 121 | class F: 122 | pass 123 | 124 | __all__ = [ 125 | "F", 126 | "hello", 127 | ] 128 | ``` 129 | 130 | * **B005: Replace "A == None" with "A is None"** 131 | 132 | "Comparisons to singletons like None should always be done with `is` or `is not`, never the equality operators." ([PEP8](https://www.python.org/dev/peps/pep-0008/)) 133 | 134 | ```python 135 | # BEFORE: 136 | 137 | if a == None: 138 | pass 139 | 140 | # AFTER: 141 | 142 | if a is None: 143 | pass 144 | ``` 145 | 146 | * **B006: Remove comparisons with either 'False' or 'True'.** 147 | 148 | [PEP8](https://www.python.org/dev/peps/pep-0008/) recommends that conditions should be evaluated without explicit equality comparison with `True`/`False` singletons. In Python, every non-empty value is treated as `True` and vice versa, 149 | 150 | so in most cases those comparisons can be safely eliminated. 151 | 152 | *NB*: `is True` and `is False` checks are not affected, since they can be used to explicitly check for equality with a specific singleton, instead of using abovementioned "non-empty" heuristic. 153 | 154 | ```python 155 | # BEFORE: 156 | 157 | if a == False or b == True or c == False == True: 158 | pass 159 | 160 | # AFTER: 161 | 162 | if not a or b or not c: 163 | pass 164 | ``` 165 | 166 | * **B007: Convert f-strings without expressions into regular strings.** 167 | 168 | It is wasteful to use f-string mechanism if there are no expressions to be extrapolated. 169 | 170 | ```python 171 | # BEFORE: 172 | a = f"Hello, world" 173 | 174 | # AFTER: 175 | a = "Hello, world" 176 | ``` 177 | 178 | * **B008: Collapse nested `with` statements** 179 | 180 | Degenerate `with` statements can be rewritten as a single compound `with` statement, if following conditions are satisfied: 181 | 182 | * There are no statements between `with` statements being collapsed; 183 | * Neither of `with` statements has any leading or inline comments. 184 | 185 | ```python 186 | # BEFORE: 187 | with a(): 188 | with b() as other_b: 189 | print("Hello, world!") 190 | 191 | # AFTER: 192 | with a(), b() as other_b: 193 | print("Hello, world!") 194 | ``` 195 | 196 | * **B009: Replace unhashable list literals in set constructors** 197 | 198 | Lists cannot be used as elements of the sets due to them being mutable and hence "unhashable". We can fix the more trivial cases of list literals being used to create a set by converting them into tuples. 199 | 200 | ```python 201 | # BEFORE: 202 | a = { 203 | [1, 2, 3], 204 | } 205 | b = set([[1, 2], ["a", "b"]]) 206 | c = frozenset([[1, 2], ["a", "b"]]) 207 | 208 | # AFTER: 209 | a = { 210 | (1, 2, 3) 211 | } 212 | b = set([(1, 2), ("a", "b")]) 213 | c = frozenset([(1, 2), ("a", "b")]) 214 | ``` 215 | 216 | * **B010: Replace 'not A is B' with 'A is not B'** 217 | 218 | Usage of `A is not B` over `not A is B` is recommended both by Google and [PEP-8](https://www.python.org/dev/peps/pep-0008/#programming-recommendations). Both of those forms are compiled to the same bytecode, but second form has some potential of confusion for the reader. 219 | (thanks to @rs2 for submitting this!). 220 | 221 | ```python 222 | # BEFORE: 223 | if not obj is Record: 224 | sys.exit(-1) 225 | 226 | # AFTER: 227 | if obj is not Record: 228 | sys.exit(-1) 229 | ``` 230 | 231 | **NB:** Each of the fixers can be disabled on per-line basis using [flake8's "noqa" comments](http://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html#in-line-ignoring-errors). 232 | 233 | ## Installation 234 | 235 | ```console 236 | pip install pybetter 237 | ``` 238 | 239 | ## Use in pre-commit hooks 240 | 241 | ```console 242 | - repo: https://github.com/lensvol/pybetter 243 | rev: release-0.4.1 244 | hooks: 245 | - id: pybetter 246 | ``` 247 | 248 | ## Getting started with development 249 | 250 | ```console 251 | git clone https://github.com/lensvol/pybetter 252 | cd pybetter 253 | poetry install 254 | ``` 255 | 256 | ## License 257 | 258 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 259 | 260 | ## Authors 261 | 262 | * **Kirill Borisov** ([lensvol@gmail.com](mailto:lensvol@gmail.com)) 263 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appnope" 3 | version = "0.1.3" 4 | description = "Disable App Nap on macOS >= 10.9" 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "attrs" 11 | version = "23.1.0" 12 | description = "Classes Without Boilerplate" 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=3.7" 16 | 17 | [package.dependencies] 18 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 19 | 20 | [package.extras] 21 | cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] 22 | dev = ["attrs[docs,tests]", "pre-commit"] 23 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] 24 | tests = ["attrs[tests-no-zope]", "zope-interface"] 25 | tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 26 | 27 | [[package]] 28 | name = "backcall" 29 | version = "0.2.0" 30 | description = "Specifications for callback functions passed in to an API" 31 | category = "dev" 32 | optional = false 33 | python-versions = "*" 34 | 35 | [[package]] 36 | name = "certifi" 37 | version = "2023.7.22" 38 | description = "Python package for providing Mozilla's CA Bundle." 39 | category = "dev" 40 | optional = false 41 | python-versions = ">=3.6" 42 | 43 | [[package]] 44 | name = "charset-normalizer" 45 | version = "3.2.0" 46 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 47 | category = "dev" 48 | optional = false 49 | python-versions = ">=3.7.0" 50 | 51 | [[package]] 52 | name = "click" 53 | version = "8.1.7" 54 | description = "Composable command line interface toolkit" 55 | category = "main" 56 | optional = false 57 | python-versions = ">=3.7" 58 | 59 | [package.dependencies] 60 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 61 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 62 | 63 | [[package]] 64 | name = "codecov" 65 | version = "2.1.13" 66 | description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" 67 | category = "dev" 68 | optional = false 69 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 70 | 71 | [package.dependencies] 72 | coverage = "*" 73 | requests = ">=2.7.9" 74 | 75 | [[package]] 76 | name = "colorama" 77 | version = "0.4.6" 78 | description = "Cross-platform colored terminal text." 79 | category = "main" 80 | optional = false 81 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 82 | 83 | [[package]] 84 | name = "coverage" 85 | version = "7.2.7" 86 | description = "Code coverage measurement for Python" 87 | category = "dev" 88 | optional = false 89 | python-versions = ">=3.7" 90 | 91 | [package.dependencies] 92 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 93 | 94 | [package.extras] 95 | toml = ["tomli"] 96 | 97 | [[package]] 98 | name = "decorator" 99 | version = "5.1.1" 100 | description = "Decorators for Humans" 101 | category = "dev" 102 | optional = false 103 | python-versions = ">=3.5" 104 | 105 | [[package]] 106 | name = "exceptiongroup" 107 | version = "1.1.3" 108 | description = "Backport of PEP 654 (exception groups)" 109 | category = "dev" 110 | optional = false 111 | python-versions = ">=3.7" 112 | 113 | [package.extras] 114 | test = ["pytest (>=6)"] 115 | 116 | [[package]] 117 | name = "flake8" 118 | version = "3.9.2" 119 | description = "the modular source code checker: pep8 pyflakes and co" 120 | category = "dev" 121 | optional = false 122 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 123 | 124 | [package.dependencies] 125 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 126 | mccabe = ">=0.6.0,<0.7.0" 127 | pycodestyle = ">=2.7.0,<2.8.0" 128 | pyflakes = ">=2.3.0,<2.4.0" 129 | 130 | [[package]] 131 | name = "hypothesis" 132 | version = "6.79.4" 133 | description = "A library for property-based testing" 134 | category = "dev" 135 | optional = false 136 | python-versions = ">=3.7" 137 | 138 | [package.dependencies] 139 | attrs = ">=19.2.0" 140 | exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 141 | sortedcontainers = ">=2.1.0,<3.0.0" 142 | 143 | [package.extras] 144 | all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "importlib-metadata (>=3.6)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2023.3)"] 145 | cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] 146 | codemods = ["libcst (>=0.3.16)"] 147 | dateutil = ["python-dateutil (>=1.4)"] 148 | django = ["django (>=3.2)"] 149 | dpcontracts = ["dpcontracts (>=0.4)"] 150 | ghostwriter = ["black (>=19.10b0)"] 151 | lark = ["lark (>=0.10.1)"] 152 | numpy = ["numpy (>=1.17.3)"] 153 | pandas = ["pandas (>=1.1)"] 154 | pytest = ["pytest (>=4.6)"] 155 | pytz = ["pytz (>=2014.1)"] 156 | redis = ["redis (>=3.0.0)"] 157 | zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2023.3)"] 158 | 159 | [[package]] 160 | name = "hypothesmith" 161 | version = "0.1.9" 162 | description = "Hypothesis strategies for generating Python programs, something like CSmith" 163 | category = "dev" 164 | optional = false 165 | python-versions = ">=3.6" 166 | 167 | [package.dependencies] 168 | hypothesis = ">=5.41.0" 169 | lark-parser = ">=0.7.2" 170 | libcst = ">=0.3.8" 171 | 172 | [[package]] 173 | name = "idna" 174 | version = "3.4" 175 | description = "Internationalized Domain Names in Applications (IDNA)" 176 | category = "dev" 177 | optional = false 178 | python-versions = ">=3.5" 179 | 180 | [[package]] 181 | name = "importlib-metadata" 182 | version = "6.7.0" 183 | description = "Read metadata from Python packages" 184 | category = "main" 185 | optional = false 186 | python-versions = ">=3.7" 187 | 188 | [package.dependencies] 189 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 190 | zipp = ">=0.5" 191 | 192 | [package.extras] 193 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 194 | perf = ["ipython"] 195 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] 196 | 197 | [[package]] 198 | name = "iniconfig" 199 | version = "2.0.0" 200 | description = "brain-dead simple config-ini parsing" 201 | category = "dev" 202 | optional = false 203 | python-versions = ">=3.7" 204 | 205 | [[package]] 206 | name = "ipdb" 207 | version = "0.13.13" 208 | description = "IPython-enabled pdb" 209 | category = "dev" 210 | optional = false 211 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 212 | 213 | [package.dependencies] 214 | decorator = {version = "*", markers = "python_version > \"3.6\""} 215 | ipython = {version = ">=7.31.1", markers = "python_version > \"3.6\""} 216 | tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""} 217 | 218 | [[package]] 219 | name = "ipython" 220 | version = "7.34.0" 221 | description = "IPython: Productive Interactive Computing" 222 | category = "dev" 223 | optional = false 224 | python-versions = ">=3.7" 225 | 226 | [package.dependencies] 227 | appnope = {version = "*", markers = "sys_platform == \"darwin\""} 228 | backcall = "*" 229 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 230 | decorator = "*" 231 | jedi = ">=0.16" 232 | matplotlib-inline = "*" 233 | pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} 234 | pickleshare = "*" 235 | prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" 236 | pygments = "*" 237 | setuptools = ">=18.5" 238 | traitlets = ">=4.2" 239 | 240 | [package.extras] 241 | all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"] 242 | doc = ["Sphinx (>=1.3)"] 243 | kernel = ["ipykernel"] 244 | nbconvert = ["nbconvert"] 245 | nbformat = ["nbformat"] 246 | notebook = ["ipywidgets", "notebook"] 247 | parallel = ["ipyparallel"] 248 | qtconsole = ["qtconsole"] 249 | test = ["ipykernel", "nbformat", "nose (>=0.10.1)", "numpy (>=1.17)", "pygments", "requests", "testpath"] 250 | 251 | [[package]] 252 | name = "jedi" 253 | version = "0.19.0" 254 | description = "An autocompletion tool for Python that can be used for text editors." 255 | category = "dev" 256 | optional = false 257 | python-versions = ">=3.6" 258 | 259 | [package.dependencies] 260 | parso = ">=0.8.3,<0.9.0" 261 | 262 | [package.extras] 263 | 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)"] 264 | qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] 265 | testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] 266 | 267 | [[package]] 268 | name = "lark-parser" 269 | version = "0.12.0" 270 | description = "a modern parsing library" 271 | category = "dev" 272 | optional = false 273 | python-versions = "*" 274 | 275 | [package.extras] 276 | atomic_cache = ["atomicwrites"] 277 | nearley = ["js2py"] 278 | regex = ["regex"] 279 | 280 | [[package]] 281 | name = "libcst" 282 | version = "1.0.1" 283 | description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10 programs." 284 | category = "main" 285 | optional = false 286 | python-versions = ">=3.7" 287 | 288 | [package.dependencies] 289 | pyyaml = ">=5.2" 290 | typing-extensions = ">=3.7.4.2" 291 | typing-inspect = ">=0.4.0" 292 | 293 | [package.extras] 294 | dev = ["Sphinx (>=5.1.1)", "black (==23.3.0)", "build (>=0.10.0)", "coverage (>=4.5.4)", "fixit (==0.1.1)", "flake8 (>=3.7.8,<5)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.2)", "jupyter (>=1.0.0)", "maturin (>=0.8.3,<0.16)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.10)", "setuptools-rust (>=1.5.2)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.1.0)", "usort (==1.0.7)"] 295 | 296 | [[package]] 297 | name = "matplotlib-inline" 298 | version = "0.1.6" 299 | description = "Inline Matplotlib backend for Jupyter" 300 | category = "dev" 301 | optional = false 302 | python-versions = ">=3.5" 303 | 304 | [package.dependencies] 305 | traitlets = "*" 306 | 307 | [[package]] 308 | name = "mccabe" 309 | version = "0.6.1" 310 | description = "McCabe checker, plugin for flake8" 311 | category = "dev" 312 | optional = false 313 | python-versions = "*" 314 | 315 | [[package]] 316 | name = "mypy" 317 | version = "1.4.1" 318 | description = "Optional static typing for Python" 319 | category = "dev" 320 | optional = false 321 | python-versions = ">=3.7" 322 | 323 | [package.dependencies] 324 | mypy-extensions = ">=1.0.0" 325 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 326 | typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} 327 | typing-extensions = ">=4.1.0" 328 | 329 | [package.extras] 330 | dmypy = ["psutil (>=4.0)"] 331 | install-types = ["pip"] 332 | python2 = ["typed-ast (>=1.4.0,<2)"] 333 | reports = ["lxml"] 334 | 335 | [[package]] 336 | name = "mypy-extensions" 337 | version = "1.0.0" 338 | description = "Type system extensions for programs checked with the mypy type checker." 339 | category = "main" 340 | optional = false 341 | python-versions = ">=3.5" 342 | 343 | [[package]] 344 | name = "packaging" 345 | version = "23.1" 346 | description = "Core utilities for Python packages" 347 | category = "dev" 348 | optional = false 349 | python-versions = ">=3.7" 350 | 351 | [[package]] 352 | name = "parso" 353 | version = "0.8.3" 354 | description = "A Python Parser" 355 | category = "dev" 356 | optional = false 357 | python-versions = ">=3.6" 358 | 359 | [package.extras] 360 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 361 | testing = ["docopt", "pytest (<6.0.0)"] 362 | 363 | [[package]] 364 | name = "pexpect" 365 | version = "4.8.0" 366 | description = "Pexpect allows easy control of interactive console applications." 367 | category = "dev" 368 | optional = false 369 | python-versions = "*" 370 | 371 | [package.dependencies] 372 | ptyprocess = ">=0.5" 373 | 374 | [[package]] 375 | name = "pickleshare" 376 | version = "0.7.5" 377 | description = "Tiny 'shelve'-like database with concurrency support" 378 | category = "dev" 379 | optional = false 380 | python-versions = "*" 381 | 382 | [[package]] 383 | name = "pluggy" 384 | version = "1.2.0" 385 | description = "plugin and hook calling mechanisms for python" 386 | category = "dev" 387 | optional = false 388 | python-versions = ">=3.7" 389 | 390 | [package.dependencies] 391 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 392 | 393 | [package.extras] 394 | dev = ["pre-commit", "tox"] 395 | testing = ["pytest", "pytest-benchmark"] 396 | 397 | [[package]] 398 | name = "prompt-toolkit" 399 | version = "3.0.39" 400 | description = "Library for building powerful interactive command lines in Python" 401 | category = "dev" 402 | optional = false 403 | python-versions = ">=3.7.0" 404 | 405 | [package.dependencies] 406 | wcwidth = "*" 407 | 408 | [[package]] 409 | name = "ptyprocess" 410 | version = "0.7.0" 411 | description = "Run a subprocess in a pseudo terminal" 412 | category = "dev" 413 | optional = false 414 | python-versions = "*" 415 | 416 | [[package]] 417 | name = "pycodestyle" 418 | version = "2.7.0" 419 | description = "Python style guide checker" 420 | category = "dev" 421 | optional = false 422 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 423 | 424 | [[package]] 425 | name = "pyemojify" 426 | version = "0.2.0" 427 | description = "Substitutes emoji aliases (like :sparkling_heart:) to emoji raw characters." 428 | category = "main" 429 | optional = false 430 | python-versions = "*" 431 | 432 | [package.dependencies] 433 | click = ">=4.1" 434 | 435 | [[package]] 436 | name = "pyflakes" 437 | version = "2.3.1" 438 | description = "passive checker of Python programs" 439 | category = "dev" 440 | optional = false 441 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 442 | 443 | [[package]] 444 | name = "Pygments" 445 | version = "2.16.1" 446 | description = "Pygments is a syntax highlighting package written in Python." 447 | category = "main" 448 | optional = false 449 | python-versions = ">=3.7" 450 | 451 | [package.extras] 452 | plugins = ["importlib-metadata"] 453 | 454 | [[package]] 455 | name = "pytest" 456 | version = "7.4.2" 457 | description = "pytest: simple powerful testing with Python" 458 | category = "dev" 459 | optional = false 460 | python-versions = ">=3.7" 461 | 462 | [package.dependencies] 463 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 464 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 465 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 466 | iniconfig = "*" 467 | packaging = "*" 468 | pluggy = ">=0.12,<2.0" 469 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 470 | 471 | [package.extras] 472 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 473 | 474 | [[package]] 475 | name = "pytest-cov" 476 | version = "4.1.0" 477 | description = "Pytest plugin for measuring coverage." 478 | category = "dev" 479 | optional = false 480 | python-versions = ">=3.7" 481 | 482 | [package.dependencies] 483 | coverage = {version = ">=5.2.1", extras = ["toml"]} 484 | pytest = ">=4.6" 485 | 486 | [package.extras] 487 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 488 | 489 | [[package]] 490 | name = "PyYAML" 491 | version = "6.0.1" 492 | description = "YAML parser and emitter for Python" 493 | category = "main" 494 | optional = false 495 | python-versions = ">=3.6" 496 | 497 | [[package]] 498 | name = "requests" 499 | version = "2.31.0" 500 | description = "Python HTTP for Humans." 501 | category = "dev" 502 | optional = false 503 | python-versions = ">=3.7" 504 | 505 | [package.dependencies] 506 | certifi = ">=2017.4.17" 507 | charset-normalizer = ">=2,<4" 508 | idna = ">=2.5,<4" 509 | urllib3 = ">=1.21.1,<3" 510 | 511 | [package.extras] 512 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 513 | use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] 514 | 515 | [[package]] 516 | name = "setuptools" 517 | version = "68.0.0" 518 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 519 | category = "dev" 520 | optional = false 521 | python-versions = ">=3.7" 522 | 523 | [package.extras] 524 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 525 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 526 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 527 | 528 | [[package]] 529 | name = "sortedcontainers" 530 | version = "2.4.0" 531 | description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" 532 | category = "dev" 533 | optional = false 534 | python-versions = "*" 535 | 536 | [[package]] 537 | name = "tomli" 538 | version = "2.0.1" 539 | description = "A lil' TOML parser" 540 | category = "dev" 541 | optional = false 542 | python-versions = ">=3.7" 543 | 544 | [[package]] 545 | name = "traitlets" 546 | version = "5.9.0" 547 | description = "Traitlets Python configuration system" 548 | category = "dev" 549 | optional = false 550 | python-versions = ">=3.7" 551 | 552 | [package.extras] 553 | docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] 554 | test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] 555 | 556 | [[package]] 557 | name = "typed-ast" 558 | version = "1.5.5" 559 | description = "a fork of Python 2 and 3 ast modules with type comment support" 560 | category = "dev" 561 | optional = false 562 | python-versions = ">=3.6" 563 | 564 | [[package]] 565 | name = "typing-extensions" 566 | version = "4.7.1" 567 | description = "Backported and Experimental Type Hints for Python 3.7+" 568 | category = "main" 569 | optional = false 570 | python-versions = ">=3.7" 571 | 572 | [[package]] 573 | name = "typing-inspect" 574 | version = "0.9.0" 575 | description = "Runtime inspection utilities for typing module." 576 | category = "main" 577 | optional = false 578 | python-versions = "*" 579 | 580 | [package.dependencies] 581 | mypy-extensions = ">=0.3.0" 582 | typing-extensions = ">=3.7.4" 583 | 584 | [[package]] 585 | name = "urllib3" 586 | version = "2.0.5" 587 | description = "HTTP library with thread-safe connection pooling, file post, and more." 588 | category = "dev" 589 | optional = false 590 | python-versions = ">=3.7" 591 | 592 | [package.extras] 593 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 594 | secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] 595 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 596 | zstd = ["zstandard (>=0.18.0)"] 597 | 598 | [[package]] 599 | name = "wcwidth" 600 | version = "0.2.7" 601 | description = "Measures the displayed width of unicode strings in a terminal" 602 | category = "dev" 603 | optional = false 604 | python-versions = "*" 605 | 606 | [[package]] 607 | name = "zipp" 608 | version = "3.15.0" 609 | description = "Backport of pathlib-compatible object wrapper for zip files" 610 | category = "main" 611 | optional = false 612 | python-versions = ">=3.7" 613 | 614 | [package.extras] 615 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 616 | testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 617 | 618 | [metadata] 619 | lock-version = "1.1" 620 | python-versions = ">3.7" 621 | content-hash = "fe5d018330c861e19b65a45289ada7a09826242568e5bd8a7ac0c93fb7685c24" 622 | 623 | [metadata.files] 624 | appnope = [ 625 | {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, 626 | {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, 627 | ] 628 | attrs = [ 629 | {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, 630 | {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, 631 | ] 632 | backcall = [ 633 | {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, 634 | {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, 635 | ] 636 | certifi = [ 637 | {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, 638 | {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, 639 | ] 640 | charset-normalizer = [ 641 | {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, 642 | {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, 643 | {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, 644 | {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, 645 | {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, 646 | {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, 647 | {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, 648 | {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, 649 | {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, 650 | {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, 651 | {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, 652 | {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, 653 | {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, 654 | {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, 655 | {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, 656 | {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, 657 | {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, 658 | {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, 659 | {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, 660 | {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, 661 | {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, 662 | {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, 663 | {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, 664 | {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, 665 | {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, 666 | {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, 667 | {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, 668 | {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, 669 | {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, 670 | {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, 671 | {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, 672 | {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, 673 | {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, 674 | {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, 675 | {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, 676 | {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, 677 | {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, 678 | {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, 679 | {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, 680 | {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, 681 | {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, 682 | {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, 683 | {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, 684 | {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, 685 | {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, 686 | {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, 687 | {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, 688 | {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, 689 | {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, 690 | {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, 691 | {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, 692 | {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, 693 | {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, 694 | {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, 695 | {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, 696 | {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, 697 | {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, 698 | {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, 699 | {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, 700 | {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, 701 | {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, 702 | {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, 703 | {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, 704 | {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, 705 | {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, 706 | {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, 707 | {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, 708 | {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, 709 | {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, 710 | {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, 711 | {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, 712 | {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, 713 | {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, 714 | {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, 715 | {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, 716 | ] 717 | click = [ 718 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 719 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 720 | ] 721 | codecov = [ 722 | {file = "codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5"}, 723 | {file = "codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c"}, 724 | ] 725 | colorama = [ 726 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 727 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 728 | ] 729 | coverage = [ 730 | {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, 731 | {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, 732 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, 733 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, 734 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, 735 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, 736 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, 737 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, 738 | {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, 739 | {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, 740 | {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, 741 | {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, 742 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, 743 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, 744 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, 745 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, 746 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, 747 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, 748 | {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, 749 | {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, 750 | {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, 751 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, 752 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, 753 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, 754 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, 755 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, 756 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, 757 | {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, 758 | {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, 759 | {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, 760 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, 761 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, 762 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, 763 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, 764 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, 765 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, 766 | {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, 767 | {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, 768 | {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, 769 | {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, 770 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, 771 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, 772 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, 773 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, 774 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, 775 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, 776 | {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, 777 | {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, 778 | {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, 779 | {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, 780 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, 781 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, 782 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, 783 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, 784 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, 785 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, 786 | {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, 787 | {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, 788 | {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, 789 | {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, 790 | ] 791 | decorator = [ 792 | {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, 793 | {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, 794 | ] 795 | exceptiongroup = [ 796 | {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, 797 | {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, 798 | ] 799 | flake8 = [ 800 | {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, 801 | {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, 802 | ] 803 | hypothesis = [ 804 | {file = "hypothesis-6.79.4-py3-none-any.whl", hash = "sha256:5ce05bc70aa4f20114effaf3375dc8b51d09a04026a0cf89d4514fc0b69f6304"}, 805 | {file = "hypothesis-6.79.4.tar.gz", hash = "sha256:e9a9ff3dc3f3eebbf214d6852882ac96ad72023f0e9770139fd3d3c1b87673e2"}, 806 | ] 807 | hypothesmith = [ 808 | {file = "hypothesmith-0.1.9-py3-none-any.whl", hash = "sha256:f5337fb5fce2f798b356daeb0721566a3e5c696d3a7873aff956753881217768"}, 809 | {file = "hypothesmith-0.1.9.tar.gz", hash = "sha256:039fd6aa0102f89df9df7ad4cff70aa8068678c13c3be2713c92568917317a04"}, 810 | ] 811 | idna = [ 812 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 813 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 814 | ] 815 | importlib-metadata = [ 816 | {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, 817 | {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, 818 | ] 819 | iniconfig = [ 820 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 821 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 822 | ] 823 | ipdb = [ 824 | {file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"}, 825 | {file = "ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726"}, 826 | ] 827 | ipython = [ 828 | {file = "ipython-7.34.0-py3-none-any.whl", hash = "sha256:c175d2440a1caff76116eb719d40538fbb316e214eda85c5515c303aacbfb23e"}, 829 | {file = "ipython-7.34.0.tar.gz", hash = "sha256:af3bdb46aa292bce5615b1b2ebc76c2080c5f77f54bda2ec72461317273e7cd6"}, 830 | ] 831 | jedi = [ 832 | {file = "jedi-0.19.0-py2.py3-none-any.whl", hash = "sha256:cb8ce23fbccff0025e9386b5cf85e892f94c9b822378f8da49970471335ac64e"}, 833 | {file = "jedi-0.19.0.tar.gz", hash = "sha256:bcf9894f1753969cbac8022a8c2eaee06bfa3724e4192470aaffe7eb6272b0c4"}, 834 | ] 835 | lark-parser = [ 836 | {file = "lark-parser-0.12.0.tar.gz", hash = "sha256:15967db1f1214013dca65b1180745047b9be457d73da224fcda3d9dd4e96a138"}, 837 | {file = "lark_parser-0.12.0-py2.py3-none-any.whl", hash = "sha256:0eaf30cb5ba787fe404d73a7d6e61df97b21d5a63ac26c5008c78a494373c675"}, 838 | ] 839 | libcst = [ 840 | {file = "libcst-1.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80423311f09fc5fc3270ede44d30d9d8d3c2d3dd50dbf703a581ca7346949fa6"}, 841 | {file = "libcst-1.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9d6dec2a3c443792e6af7c36fadc256e4ea586214c76b52f0d18118811dbe351"}, 842 | {file = "libcst-1.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4840a3de701778f0a19582bb3085c61591329153f801dc25da84689a3733960b"}, 843 | {file = "libcst-1.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0138068baf09561268c7f079373bda45f0e2b606d2d19df1307ca8a5134fc465"}, 844 | {file = "libcst-1.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a4931feceab171e6fce73de94e13880424367247dad6ff2b49cabfec733e144"}, 845 | {file = "libcst-1.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:47dba43855e9c7b06d8b256ee81f0ebec6a4f43605456519577e09dfe4b4288c"}, 846 | {file = "libcst-1.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c50541c3fd6b1d5a3765c4bb5ee8ecbba9d0e798e48f79fd5adf3b6752de4d0"}, 847 | {file = "libcst-1.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5599166d5fec40e18601fb8868519dde99f77b6e4ad6074958018f9545da7abd"}, 848 | {file = "libcst-1.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600c4d3a9a2f75d5a055fed713a5a4d812709947909610aa6527abe08a31896f"}, 849 | {file = "libcst-1.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b5aea04c35e13109edad3cf83bc6dcd74309b150a781d2189eecb288b73a87"}, 850 | {file = "libcst-1.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddd4e0eeec499d1c824ab545e62e957dbbd69a16bc4273208817638eb7d6b3c6"}, 851 | {file = "libcst-1.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:414350df5e334ddf0db1732d63da44e81b734d45abe1c597b5e5c0dd46aa4156"}, 852 | {file = "libcst-1.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1adcfa7cafb6a0d39a1a0bec541355608038b45815e0c5019c95f91921d42884"}, 853 | {file = "libcst-1.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d31ce2790eab59c1bd8e33fe72d09cfc78635c145bdc3f08296b360abb5f443"}, 854 | {file = "libcst-1.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2cb687e1514625e91024e50a5d2e485c0ad3be24f199874ebf32b5de0346150"}, 855 | {file = "libcst-1.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6caa33430c0c7a0fcad921b0deeec61ddb96796b6f88dca94966f6db62065f4f"}, 856 | {file = "libcst-1.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b97f652b15c50e91df411a9c8d5e6f75882b30743a49b387dcedd3f68ed94d75"}, 857 | {file = "libcst-1.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:967c66fabd52102954207bf1541312b467afc210fdf7033f32da992fb6c2372c"}, 858 | {file = "libcst-1.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b666a605f4205c8357696f3b6571a38f6a8537cdcbb8f357587d35168298af34"}, 859 | {file = "libcst-1.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae49dcbfadefb82e830d41d9f0a1db0af3b771224768f431f1b7b3a9803ed7e3"}, 860 | {file = "libcst-1.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c90c74a8a314f0774f045122323fb60bacba79cbf5f71883c0848ecd67179541"}, 861 | {file = "libcst-1.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0533de4e35396c61aeb3a6266ac30369a855910c2385aaa902ff4aabd60d409"}, 862 | {file = "libcst-1.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:5e3293e77657ba62533553bb9f0c5fb173780e164c65db1ea2a3e0d03944a284"}, 863 | {file = "libcst-1.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:119ba709f1dcb785a4458cf36cedb51d6f9cb2eec0acd7bb171f730eac7cb6ce"}, 864 | {file = "libcst-1.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b4e336f6d68456017671cdda8ddebf9caebce8052cc21a3f494b03d7bd28386"}, 865 | {file = "libcst-1.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8420926791b0b6206cb831a7ec73d26ae820e65bdf07ce9813c7754c7722c07a"}, 866 | {file = "libcst-1.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d237e9164a43caa7d6765ee560412264484e7620c546a2ee10a8d01bd56884e0"}, 867 | {file = "libcst-1.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:440887e5f82efb299f2e98d4bfa5663851a878cfc0efed652ab8c50205191436"}, 868 | {file = "libcst-1.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:ae7f4e71d714f256b5f2ff98b5a9effba0f9dff4d779d8f35d7eb157bef78f59"}, 869 | {file = "libcst-1.0.1.tar.gz", hash = "sha256:37187337f979ba426d8bfefc08008c3c1b09b9e9f9387050804ed2da88107570"}, 870 | ] 871 | matplotlib-inline = [ 872 | {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, 873 | {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, 874 | ] 875 | mccabe = [ 876 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 877 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 878 | ] 879 | mypy = [ 880 | {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, 881 | {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, 882 | {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, 883 | {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, 884 | {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, 885 | {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, 886 | {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, 887 | {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, 888 | {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, 889 | {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, 890 | {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, 891 | {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, 892 | {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, 893 | {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, 894 | {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, 895 | {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, 896 | {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, 897 | {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, 898 | {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, 899 | {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, 900 | {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, 901 | {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, 902 | {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, 903 | {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, 904 | {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, 905 | {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, 906 | ] 907 | mypy-extensions = [ 908 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 909 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 910 | ] 911 | packaging = [ 912 | {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, 913 | {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, 914 | ] 915 | parso = [ 916 | {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, 917 | {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, 918 | ] 919 | pexpect = [ 920 | {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, 921 | {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, 922 | ] 923 | pickleshare = [ 924 | {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, 925 | {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, 926 | ] 927 | pluggy = [ 928 | {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, 929 | {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, 930 | ] 931 | prompt-toolkit = [ 932 | {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, 933 | {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, 934 | ] 935 | ptyprocess = [ 936 | {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, 937 | {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, 938 | ] 939 | pycodestyle = [ 940 | {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, 941 | {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, 942 | ] 943 | pyemojify = [ 944 | {file = "pyemojify-0.2.0-py2.py3-none-any.whl", hash = "sha256:e70e4cfcfe0aed7b5bc64f39b023d5d62a5f5c0c31c1b7114cd43a059fb14a72"}, 945 | {file = "pyemojify-0.2.0.tar.gz", hash = "sha256:6bbc3c8d52e3df3e4039bc0cad3616d3eb579b4c6e15a11bd5e0ef0d579596a9"}, 946 | ] 947 | pyflakes = [ 948 | {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, 949 | {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, 950 | ] 951 | Pygments = [ 952 | {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, 953 | {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, 954 | ] 955 | pytest = [ 956 | {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, 957 | {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, 958 | ] 959 | pytest-cov = [ 960 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 961 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 962 | ] 963 | PyYAML = [ 964 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 965 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 966 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 967 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 968 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 969 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 970 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 971 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 972 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 973 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 974 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 975 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 976 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 977 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 978 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 979 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 980 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 981 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 982 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 983 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 984 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 985 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 986 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 987 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 988 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 989 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 990 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 991 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 992 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 993 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 994 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 995 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 996 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 997 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 998 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 999 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 1000 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 1001 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 1002 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 1003 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 1004 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 1005 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 1006 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 1007 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 1008 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 1009 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 1010 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 1011 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 1012 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 1013 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 1014 | ] 1015 | requests = [ 1016 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 1017 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 1018 | ] 1019 | setuptools = [ 1020 | {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, 1021 | {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, 1022 | ] 1023 | sortedcontainers = [ 1024 | {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, 1025 | {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, 1026 | ] 1027 | tomli = [ 1028 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1029 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1030 | ] 1031 | traitlets = [ 1032 | {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"}, 1033 | {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"}, 1034 | ] 1035 | typed-ast = [ 1036 | {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, 1037 | {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, 1038 | {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, 1039 | {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"}, 1040 | {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"}, 1041 | {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"}, 1042 | {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"}, 1043 | {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"}, 1044 | {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"}, 1045 | {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"}, 1046 | {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"}, 1047 | {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"}, 1048 | {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"}, 1049 | {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"}, 1050 | {file = "typed_ast-1.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5"}, 1051 | {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c"}, 1052 | {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa"}, 1053 | {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f"}, 1054 | {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d"}, 1055 | {file = "typed_ast-1.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5"}, 1056 | {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"}, 1057 | {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"}, 1058 | {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"}, 1059 | {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"}, 1060 | {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"}, 1061 | {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"}, 1062 | {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"}, 1063 | {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"}, 1064 | {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"}, 1065 | {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"}, 1066 | {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"}, 1067 | {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"}, 1068 | {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"}, 1069 | {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"}, 1070 | {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"}, 1071 | {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"}, 1072 | {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"}, 1073 | {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"}, 1074 | {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"}, 1075 | {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, 1076 | {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, 1077 | ] 1078 | typing-extensions = [ 1079 | {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, 1080 | {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, 1081 | ] 1082 | typing-inspect = [ 1083 | {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, 1084 | {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, 1085 | ] 1086 | urllib3 = [ 1087 | {file = "urllib3-2.0.5-py3-none-any.whl", hash = "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e"}, 1088 | {file = "urllib3-2.0.5.tar.gz", hash = "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594"}, 1089 | ] 1090 | wcwidth = [ 1091 | {file = "wcwidth-0.2.7-py2.py3-none-any.whl", hash = "sha256:fabf3e32999d9b0dab7d19d845149f326f04fe29bac67709ee071dbd92640a36"}, 1092 | {file = "wcwidth-0.2.7.tar.gz", hash = "sha256:1b6d30a98ddd5ce9bbdb33658191fd2423fc9da203fe3ef1855407dcb7ee4e26"}, 1093 | ] 1094 | zipp = [ 1095 | {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, 1096 | {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, 1097 | ] 1098 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /pybetter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lensvol/pybetter/fe67efea2b973f7230bf0b89bc28cdef746c2b18/pybetter/__init__.py -------------------------------------------------------------------------------- /pybetter/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from pybetter.cli import main 4 | 5 | if __name__ == "__main__": 6 | sys.exit(main(prog_name="pybetter")) 7 | -------------------------------------------------------------------------------- /pybetter/cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | from typing import List, FrozenSet, Tuple, Type, Iterable 4 | 5 | import libcst as cst 6 | import click 7 | from pyemojify import emojify 8 | 9 | from pybetter.improvements import ( 10 | FixNotInConditionOrder, 11 | BaseImprovement, 12 | FixMutableDefaultArgs, 13 | FixParenthesesInReturn, 14 | FixMissingAllAttribute, 15 | FixEqualsNone, 16 | FixBooleanEqualityChecks, 17 | FixTrivialFmtStringCreation, 18 | FixTrivialNestedWiths, 19 | FixUnhashableList, 20 | FixNotIsConditionOrder, 21 | ) 22 | from pybetter.utils import resolve_paths, create_diff, prettify_time_interval 23 | 24 | ALL_IMPROVEMENTS = ( 25 | FixNotInConditionOrder, 26 | FixMutableDefaultArgs, 27 | FixParenthesesInReturn, 28 | FixMissingAllAttribute, 29 | FixEqualsNone, 30 | FixBooleanEqualityChecks, 31 | FixTrivialFmtStringCreation, 32 | FixTrivialNestedWiths, 33 | FixUnhashableList, 34 | FixNotIsConditionOrder, 35 | ) 36 | 37 | 38 | def filter_improvements_by_code(code_list: str) -> FrozenSet[str]: 39 | all_codes = frozenset([improvement.CODE for improvement in ALL_IMPROVEMENTS]) 40 | codes = frozenset([code.strip() for code in code_list.split(",")]) - {""} 41 | 42 | if not codes: 43 | return frozenset() 44 | 45 | wrong_codes = codes.difference(all_codes) 46 | if wrong_codes: 47 | print( 48 | emojify( 49 | f":no_entry_sign: Unknown improvements selected: {','.join(wrong_codes)}" 50 | ) 51 | ) 52 | return frozenset() 53 | 54 | return codes 55 | 56 | 57 | def process_file( 58 | source: str, improvements: Iterable[Type[BaseImprovement]] 59 | ) -> Tuple[str, List[BaseImprovement]]: 60 | tree: cst.Module = cst.parse_module(source) 61 | modified_tree: cst.Module = tree 62 | improvements_applied = [] 63 | 64 | for case_cls in improvements: 65 | intermediate_tree = modified_tree 66 | case = case_cls() 67 | modified_tree = case.improve(intermediate_tree) 68 | 69 | if not modified_tree.deep_equals(intermediate_tree): 70 | improvements_applied.append(case) 71 | 72 | return modified_tree.code, improvements_applied 73 | 74 | 75 | @click.group() 76 | def cli(): 77 | pass 78 | 79 | 80 | @cli.command() 81 | @click.option( 82 | "--noop", 83 | is_flag=True, 84 | default=False, 85 | help="Do not make any changes to the source files.", 86 | ) 87 | @click.option( 88 | "--diff", 89 | "show_diff", 90 | is_flag=True, 91 | default=False, 92 | help="Show diff-like output of the changes made.", 93 | ) 94 | @click.option( 95 | "--select", 96 | "selected", 97 | type=str, 98 | metavar="CODES", 99 | help="Apply only improvements with the provided codes.", 100 | ) 101 | @click.option( 102 | "--exclude", 103 | "excluded", 104 | type=str, 105 | metavar="CODES", 106 | help="Exclude improvements with the provided codes.", 107 | ) 108 | @click.option( 109 | "--exit-code", 110 | "exit_code", 111 | type=int, 112 | metavar="", 113 | default=0, 114 | help="Exit with provided code if fixes were applied.", 115 | ) 116 | @click.argument("paths", type=click.Path(), nargs=-1) 117 | def main( 118 | paths, noop: bool, show_diff: bool, selected: str, excluded: str, exit_code: int 119 | ): 120 | if not paths: 121 | print(emojify("Nothing to do. :sleeping:")) 122 | return 123 | 124 | selected_improvements = list(ALL_IMPROVEMENTS) 125 | 126 | if selected and excluded: 127 | print( 128 | emojify( 129 | ":no_entry_sign: '--select' and '--exclude' options are mutually exclusive!" 130 | ) 131 | ) 132 | return 133 | 134 | if selected: 135 | selected_codes = filter_improvements_by_code(selected) 136 | 137 | selected_improvements = [ 138 | improvement 139 | for improvement in ALL_IMPROVEMENTS 140 | if improvement.CODE in selected_codes 141 | ] 142 | elif excluded: 143 | excluded_codes = filter_improvements_by_code(excluded) 144 | 145 | selected_improvements = [ 146 | improvement 147 | for improvement in ALL_IMPROVEMENTS 148 | if improvement.CODE not in excluded_codes 149 | ] 150 | 151 | if not selected_improvements: 152 | print(emojify(":sleeping: No improvements to apply.")) 153 | return 154 | 155 | python_files = filter(lambda fn: fn.endswith(".py"), resolve_paths(*paths)) 156 | 157 | are_fixes_applied = False 158 | total_start_ts = time.process_time() 159 | for path_to_source in python_files: 160 | with open(path_to_source, "r+") as source_file: 161 | original_source: str = source_file.read() 162 | 163 | start_ts = time.process_time() 164 | processed_source, applied = process_file( 165 | original_source, selected_improvements 166 | ) 167 | end_ts = time.process_time() 168 | 169 | if original_source == processed_source: 170 | continue 171 | 172 | print(f"--> Fixed '{source_file.name}'...") 173 | for case in applied: 174 | print(f" [+] ({case.CODE}) {case.DESCRIPTION}") 175 | print() 176 | print(f" Time taken: {end_ts - start_ts:.2f} seconds") 177 | 178 | if show_diff: 179 | print() 180 | print( 181 | create_diff( 182 | original_source, 183 | processed_source, 184 | source_file.name, 185 | # ANSI codes used for highlighting source code 186 | # will be saved to file and cause `patch` to bail out. 187 | highlight=sys.stderr.isatty(), 188 | ), 189 | file=sys.stderr, 190 | ) 191 | 192 | are_fixes_applied = True 193 | 194 | if noop: 195 | continue 196 | 197 | source_file.seek(0) 198 | source_file.truncate() 199 | source_file.write(processed_source) 200 | 201 | print() 202 | 203 | time_taken = prettify_time_interval(time.process_time() - total_start_ts) 204 | print(emojify(f":sparkles: All done! :sparkles: :clock2: {time_taken}")) 205 | 206 | if are_fixes_applied: 207 | sys.exit(exit_code) 208 | 209 | 210 | __all__ = ["main", "process_file", "ALL_IMPROVEMENTS"] 211 | -------------------------------------------------------------------------------- /pybetter/improvements.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | import libcst as cst 4 | from libcst import MetadataWrapper 5 | from typing_extensions import Type 6 | 7 | from pybetter.transformers.all_attribute import AllAttributeTransformer 8 | from pybetter.transformers.base import NoqaDetectionVisitor, NoqaAwareTransformer 9 | from pybetter.transformers.boolean_equality import BooleanLiteralEqualityTransformer 10 | from pybetter.transformers.empty_fstring import TrivialFmtStringTransformer 11 | from pybetter.transformers.equals_none import EqualsNoneIsNoneTransformer 12 | from pybetter.transformers.mutable_args import ArgEmptyInitTransformer 13 | from pybetter.transformers.nested_withs import NestedWithTransformer 14 | from pybetter.transformers.not_in import NotInConditionTransformer 15 | from pybetter.transformers.not_is import NotIsConditionTransformer 16 | from pybetter.transformers.parenthesized_return import RemoveParenthesesFromReturn 17 | from pybetter.transformers.unhashable_list import UnhashableListTransformer 18 | 19 | 20 | class BaseImprovement(ABC): 21 | CODE: str 22 | NAME: str 23 | DESCRIPTION: str 24 | TRANSFORMER: Type[NoqaAwareTransformer] 25 | 26 | def improve(self, tree: cst.Module): 27 | noqa_detector = NoqaDetectionVisitor() 28 | wrapper = MetadataWrapper(tree) 29 | 30 | with noqa_detector.resolve(wrapper): 31 | wrapper.visit(noqa_detector) 32 | transformer = self.TRANSFORMER(self.CODE, noqa_detector.get_noqa_lines()) 33 | return wrapper.visit(transformer) 34 | 35 | 36 | class FixNotInConditionOrder(BaseImprovement): 37 | NAME = "not_in" 38 | DESCRIPTION = "Replace 'not A in B' with 'A not in B'" 39 | CODE = "B001" 40 | TRANSFORMER = NotInConditionTransformer 41 | 42 | 43 | class FixMutableDefaultArgs(BaseImprovement): 44 | NAME = "mutable_defaults" 45 | DESCRIPTION = "Default values for **kwargs are mutable." 46 | CODE = "B002" 47 | TRANSFORMER = ArgEmptyInitTransformer 48 | 49 | 50 | class FixParenthesesInReturn(BaseImprovement): 51 | NAME = "parenthesis_return" 52 | DESCRIPTION = "Remove parentheses from the tuple in 'return' statement." 53 | CODE = "B003" 54 | TRANSFORMER = RemoveParenthesesFromReturn 55 | 56 | 57 | class FixMissingAllAttribute(BaseImprovement): 58 | NAME = "missing_all" 59 | DESCRIPTION = "__all__ attribute is missing." 60 | CODE = "B004" 61 | TRANSFORMER = AllAttributeTransformer 62 | 63 | 64 | class FixEqualsNone(BaseImprovement): 65 | NAME = "equals_none" 66 | DESCRIPTION = "Replace 'a == None' with 'a is None'." 67 | CODE = "B005" 68 | TRANSFORMER = EqualsNoneIsNoneTransformer 69 | 70 | 71 | class FixBooleanEqualityChecks(BaseImprovement): 72 | NAME = "false_true_equality" 73 | DESCRIPTION = "Remove comparisons with either 'False' or 'True'." 74 | CODE = "B006" 75 | TRANSFORMER = BooleanLiteralEqualityTransformer 76 | 77 | 78 | class FixTrivialFmtStringCreation(BaseImprovement): 79 | NAME = "trivial_fstring" 80 | DESCRIPTION = "Convert f-strings without expressions into regular strings." 81 | CODE = "B007" 82 | TRANSFORMER = TrivialFmtStringTransformer 83 | 84 | 85 | class FixTrivialNestedWiths(BaseImprovement): 86 | NAME = "nested_withs" 87 | DESCRIPTION = "Replace nested 'with' statements with a compound one." 88 | CODE = "B008" 89 | TRANSFORMER = NestedWithTransformer 90 | 91 | 92 | class FixUnhashableList(BaseImprovement): 93 | NAME = "unhashable_list" 94 | DESCRIPTION = "Replace unhashable list literals in sets with tuples." 95 | CODE = "B009" 96 | TRANSFORMER = UnhashableListTransformer 97 | 98 | 99 | class FixNotIsConditionOrder(BaseImprovement): 100 | NAME = "not_is" 101 | DESCRIPTION = "Replace 'not A is B' with 'A is not B'" 102 | CODE = "B010" 103 | TRANSFORMER = NotIsConditionTransformer 104 | 105 | 106 | __all__ = [ 107 | "BaseImprovement", 108 | "FixBooleanEqualityChecks", 109 | "FixEqualsNone", 110 | "FixMissingAllAttribute", 111 | "FixMutableDefaultArgs", 112 | "FixNotInConditionOrder", 113 | "FixParenthesesInReturn", 114 | "FixTrivialFmtStringCreation", 115 | "FixTrivialNestedWiths", 116 | "FixUnhashableList", 117 | "FixNotIsConditionOrder", 118 | ] 119 | -------------------------------------------------------------------------------- /pybetter/transformers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lensvol/pybetter/fe67efea2b973f7230bf0b89bc28cdef746c2b18/pybetter/transformers/__init__.py -------------------------------------------------------------------------------- /pybetter/transformers/all_attribute.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | import libcst as cst 4 | from libcst.metadata import ScopeProvider, GlobalScope 5 | import libcst.matchers as m 6 | 7 | from pybetter.transformers.base import NoqaAwareTransformer 8 | 9 | 10 | class AllAttributeTransformer(NoqaAwareTransformer): 11 | METADATA_DEPENDENCIES = (ScopeProvider,) # type: ignore 12 | 13 | def __init__(self, *args, **kwargs): 14 | super().__init__(*args, **kwargs) 15 | self.names = [] 16 | self.already_exists = False 17 | 18 | def process_node( 19 | self, 20 | node: Union[cst.FunctionDef, cst.ClassDef, cst.BaseAssignTargetExpression], 21 | name: str, 22 | ) -> None: 23 | scope = self.get_metadata(ScopeProvider, node) 24 | if isinstance(scope, GlobalScope) and not name.startswith("_"): 25 | self.names.append(name) 26 | 27 | def visit_AssignTarget(self, node: cst.AssignTarget) -> Optional[bool]: 28 | if m.matches(node, m.AssignTarget(target=m.Name())): 29 | target = cst.ensure_type(node.target, cst.Name) 30 | if target.value == "__all__": 31 | self.already_exists = True 32 | else: 33 | self.process_node(node.target, target.value) 34 | return None 35 | 36 | def visit_FunctionDef(self, node: cst.FunctionDef) -> Optional[bool]: 37 | self.process_node(node, node.name.value) 38 | return None 39 | 40 | def visit_ClassDef(self, node: cst.ClassDef) -> Optional[bool]: 41 | self.process_node(node, node.name.value) 42 | return None 43 | 44 | def leave_Module( 45 | self, original_node: cst.Module, updated_node: cst.Module 46 | ) -> cst.Module: 47 | if not self.names or self.already_exists: 48 | return original_node 49 | 50 | modified_body = list(original_node.body) 51 | config = original_node.config_for_parsing 52 | 53 | list_of_names = f",{config.default_newline}{config.default_indent}".join( 54 | [repr(name) for name in sorted(dict.fromkeys(self.names))] 55 | ) 56 | 57 | all_names = cst.parse_statement( 58 | f""" 59 | 60 | __all__ = [ 61 | {config.default_indent}{list_of_names} 62 | ] 63 | """, 64 | config=original_node.config_for_parsing, 65 | ) 66 | 67 | modified_body.append(all_names) 68 | return updated_node.with_changes(body=modified_body) 69 | 70 | 71 | __all__ = ["AllAttributeTransformer"] 72 | -------------------------------------------------------------------------------- /pybetter/transformers/base.py: -------------------------------------------------------------------------------- 1 | import re 2 | from abc import ABCMeta 3 | from typing import Dict, Optional, FrozenSet 4 | 5 | import libcst as cst 6 | from libcst.matchers import MatcherDecoratableTransformer 7 | from libcst.metadata import PositionProvider 8 | 9 | NOQA_MARKUP_REGEX = re.compile(r"noqa(?:: ((?:B[0-9]{3},)+(?:B[0-9]{3})|(B[0-9]{3})))?") 10 | NOQA_CATCHALL: str = "B999" 11 | 12 | NoqaLineMapping = Dict[int, FrozenSet[str]] 13 | 14 | 15 | class NoqaDetectionVisitor(cst.CSTVisitor): 16 | METADATA_DEPENDENCIES = (PositionProvider,) 17 | 18 | def __init__(self): 19 | self._line_to_code: NoqaLineMapping = {} 20 | 21 | super().__init__() 22 | 23 | def visit_Comment(self, node: cst.Comment) -> Optional[bool]: 24 | m = re.search(NOQA_MARKUP_REGEX, node.value) 25 | if m: 26 | codes = m.group(1) 27 | position: cst.metadata.CodeRange = self.get_metadata(PositionProvider, node) 28 | if codes: 29 | self._line_to_code[position.start.line] = frozenset(codes.split(",")) 30 | else: 31 | self._line_to_code[position.start.line] = frozenset({NOQA_CATCHALL}) 32 | 33 | return True 34 | 35 | def get_noqa_lines(self) -> NoqaLineMapping: 36 | return self._line_to_code 37 | 38 | 39 | class PositionProviderEnsuranceMetaclass(ABCMeta): 40 | def __new__(cls, name, bases, attrs): 41 | providers = attrs.get("METADATA_DEPENDENCIES", ()) 42 | if PositionProvider not in providers: 43 | attrs["METADATA_DEPENDENCIES"] = (PositionProvider,) + providers 44 | 45 | return super().__new__(cls, name, bases, attrs) 46 | 47 | 48 | class NoqaAwareTransformer( 49 | MatcherDecoratableTransformer, metaclass=PositionProviderEnsuranceMetaclass 50 | ): 51 | METADATA_DEPENDENCIES = (PositionProvider,) # type: ignore 52 | 53 | def __init__(self, code: str, noqa_lines: NoqaLineMapping): 54 | self.check_code: str = code 55 | self.noqa_lines: NoqaLineMapping = noqa_lines 56 | super().__init__() 57 | 58 | def on_visit(self, node: cst.CSTNode): 59 | position: cst.metadata.CodeRange = self.get_metadata(PositionProvider, node) 60 | applicable_noqa: FrozenSet[str] = self.noqa_lines.get( 61 | position.start.line, frozenset() 62 | ) 63 | 64 | if self.check_code in applicable_noqa or NOQA_CATCHALL in applicable_noqa: 65 | return False 66 | 67 | return super().on_visit(node) 68 | 69 | 70 | __all__ = ["NoqaAwareTransformer", "NoqaDetectionVisitor", "NoqaLineMapping"] 71 | -------------------------------------------------------------------------------- /pybetter/transformers/boolean_equality.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import libcst as cst 4 | from libcst import matchers as m 5 | 6 | from pybetter.transformers.base import NoqaAwareTransformer 7 | 8 | 9 | class BooleanLiteralEqualityTransformer(NoqaAwareTransformer): 10 | def leave_Comparison( 11 | self, original_node: cst.Comparison, updated_node: cst.Comparison 12 | ) -> cst.BaseExpression: 13 | remaining_targets: List[cst.ComparisonTarget] = [] 14 | 15 | for target in original_node.comparisons: 16 | if m.matches( 17 | target, 18 | m.ComparisonTarget(comparator=m.Name("False"), operator=m.Equal()), 19 | ): 20 | return cst.UnaryOperation( 21 | operator=cst.Not(), expression=original_node.left 22 | ) 23 | 24 | if not m.matches( 25 | target, 26 | m.ComparisonTarget(comparator=m.Name("True"), operator=m.Equal()), 27 | ): 28 | remaining_targets.append(target) 29 | 30 | # FIXME: Explicitly check for `a == False == True ...` case and 31 | # short-circuit it to `not a`. 32 | 33 | if not remaining_targets: 34 | return original_node.left 35 | 36 | return updated_node.with_changes(comparisons=remaining_targets) 37 | 38 | 39 | __all__ = ["BooleanLiteralEqualityTransformer"] 40 | -------------------------------------------------------------------------------- /pybetter/transformers/empty_fstring.py: -------------------------------------------------------------------------------- 1 | import libcst as cst 2 | 3 | from pybetter.transformers.base import NoqaAwareTransformer 4 | 5 | 6 | class TrivialFmtStringTransformer(NoqaAwareTransformer): 7 | def leave_FormattedString( 8 | self, original_node: cst.FormattedString, updated_node: cst.FormattedString 9 | ) -> cst.BaseExpression: 10 | if len(updated_node.parts) == 1 and isinstance( 11 | updated_node.parts[0], cst.FormattedStringText 12 | ): 13 | # We need to explicitly specify quotation marks here, otherwise we 14 | # will fail SimpleString's internal validation. This is due to 15 | # SimpleString._get_prefix treating everything before quotation 16 | # marks as a prefix. (sic!) 17 | return cst.SimpleString( 18 | value=f'{updated_node.start.replace("f", "")}{updated_node.parts[0].value}{updated_node.end}' 19 | ) 20 | 21 | return original_node 22 | 23 | 24 | __all__ = ["TrivialFmtStringTransformer"] 25 | -------------------------------------------------------------------------------- /pybetter/transformers/equals_none.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import libcst as cst 4 | from libcst import matchers as m 5 | 6 | from pybetter.transformers.base import NoqaAwareTransformer 7 | 8 | 9 | class EqualsNoneIsNoneTransformer(NoqaAwareTransformer): 10 | @m.leave(m.ComparisonTarget(comparator=m.Name(value="None"), operator=m.Equal())) 11 | def convert_none_cmp( 12 | self, _, updated_node: cst.ComparisonTarget 13 | ) -> Union[cst.ComparisonTarget, cst.RemovalSentinel]: 14 | original_op = cst.ensure_type(updated_node.operator, cst.Equal) 15 | 16 | return updated_node.with_changes( 17 | operator=cst.Is( 18 | whitespace_after=original_op.whitespace_after, 19 | whitespace_before=original_op.whitespace_before, 20 | ) 21 | ) 22 | 23 | 24 | __all__ = ["EqualsNoneIsNoneTransformer"] 25 | -------------------------------------------------------------------------------- /pybetter/transformers/mutable_args.py: -------------------------------------------------------------------------------- 1 | from itertools import takewhile, dropwhile 2 | from typing import Union, List, Optional, Tuple 3 | 4 | import libcst as cst 5 | from libcst import matchers as m 6 | from libcst.helpers import parse_template_statement 7 | 8 | from pybetter.transformers.base import NoqaAwareTransformer 9 | 10 | DEFAULT_INIT_TEMPLATE = """if {arg} is None: 11 | {arg} = {init} 12 | """ 13 | # If you do not explicitly set `indent` to False, then even empty line 14 | # will contain at least one indent worth of whitespaces. 15 | EMPTY_LINE = cst.EmptyLine(indent=False, newline=cst.Newline()) 16 | 17 | 18 | def is_docstring(node): 19 | return m.matches(node, m.SimpleStatementLine(body=[m.Expr(value=m.SimpleString())])) 20 | 21 | 22 | class ArgEmptyInitTransformer(NoqaAwareTransformer): 23 | def __init__(self, *args, **kwargs): 24 | super().__init__(*args, **kwargs) 25 | self.module_config = None 26 | 27 | def visit_Module(self, node: cst.Module) -> Optional[bool]: 28 | self.module_config = node.config_for_parsing 29 | return True 30 | 31 | def leave_FunctionDef( 32 | self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef 33 | ) -> Union[cst.BaseStatement, cst.RemovalSentinel]: 34 | modified_defaults: List = [] 35 | mutable_args: List[Tuple[cst.Name, Union[cst.List, cst.Dict]]] = [] 36 | 37 | for param in updated_node.params.params: 38 | if not m.matches(param, m.Param(default=m.OneOf(m.List(), m.Dict()))): 39 | modified_defaults.append(param) 40 | continue 41 | 42 | # This line here is just for type checkers peace of mind, 43 | # since it cannot reason about variables from matchers result. 44 | if not isinstance(param.default, (cst.List, cst.Dict)): 45 | continue 46 | 47 | mutable_args.append((param.name, param.default)) 48 | modified_defaults.append( 49 | param.with_changes( 50 | default=cst.Name("None"), 51 | ) 52 | ) 53 | 54 | if not mutable_args: 55 | return original_node 56 | 57 | modified_params: cst.Parameters = updated_node.params.with_changes( 58 | params=modified_defaults 59 | ) 60 | 61 | initializations: List[ 62 | Union[cst.SimpleStatementLine, cst.BaseCompoundStatement] 63 | ] = [ 64 | # We use generation by template here since construction of the 65 | # resulting 'if' can be burdensome due to many nested objects 66 | # involved. Additional line is attached so that we may control 67 | # exact spacing between generated statements. 68 | parse_template_statement( 69 | DEFAULT_INIT_TEMPLATE, config=self.module_config, arg=arg, init=init 70 | ).with_changes(leading_lines=[EMPTY_LINE]) 71 | for arg, init in mutable_args 72 | ] 73 | 74 | # Docstring should always go right after the function definition, 75 | # so we take special care to insert our initializations after the 76 | # last docstring found. 77 | docstrings = takewhile(is_docstring, updated_node.body.body) 78 | function_code = dropwhile(is_docstring, updated_node.body.body) 79 | 80 | # It is not possible to insert empty line after the statement line, 81 | # because whitespace is owned by the next statement after it. 82 | stmt_with_empty_line = next(function_code).with_changes( 83 | leading_lines=[EMPTY_LINE] 84 | ) 85 | 86 | modified_body = ( 87 | *docstrings, 88 | *initializations, 89 | stmt_with_empty_line, 90 | *function_code, 91 | ) 92 | 93 | return updated_node.with_changes( 94 | params=modified_params, 95 | body=updated_node.body.with_changes(body=modified_body), 96 | ) 97 | 98 | 99 | __all__ = ["ArgEmptyInitTransformer"] 100 | -------------------------------------------------------------------------------- /pybetter/transformers/nested_withs.py: -------------------------------------------------------------------------------- 1 | from typing import Union, List 2 | 3 | import libcst as cst 4 | import libcst.matchers as m 5 | 6 | from pybetter.transformers.base import NoqaAwareTransformer 7 | 8 | 9 | def has_leading_comment(node: Union[cst.SimpleStatementLine, cst.With]) -> bool: 10 | return any([line.comment is not None for line in node.leading_lines]) 11 | 12 | 13 | def has_inline_comment(node: cst.BaseSuite): 14 | return m.matches( 15 | node, 16 | m.IndentedBlock( 17 | header=m.AllOf( 18 | m.TrailingWhitespace(), m.MatchIfTrue(lambda h: h.comment is not None) 19 | ) 20 | ), 21 | ) 22 | 23 | 24 | def has_footer_comment(body): 25 | return m.matches( 26 | body, 27 | m.IndentedBlock( 28 | footer=[m.ZeroOrMore(), m.EmptyLine(comment=m.Comment()), m.ZeroOrMore()] 29 | ), 30 | ) 31 | 32 | 33 | class NestedWithTransformer(NoqaAwareTransformer): 34 | def leave_With( 35 | self, original_node: cst.With, updated_node: cst.With 36 | ) -> Union[cst.BaseStatement, cst.RemovalSentinel]: 37 | candidate_with: cst.With = original_node 38 | compound_items: List[cst.WithItem] = [] 39 | final_body: cst.BaseSuite = candidate_with.body 40 | 41 | while True: 42 | # There is no way to meaningfully represent comments inside 43 | # multi-line `with` statements due to how Python grammar is 44 | # written, so we do not try to transform such `with` statements 45 | # lest we lose something important in the comments. 46 | if has_leading_comment(candidate_with): 47 | break 48 | 49 | if has_inline_comment(candidate_with.body): 50 | break 51 | 52 | # There is no meaningful way `async with` can be merged into 53 | # the compound `with` statement. 54 | if candidate_with.asynchronous: 55 | break 56 | 57 | compound_items.extend(candidate_with.items) 58 | final_body = candidate_with.body 59 | 60 | if not isinstance(final_body.body[0], cst.With): 61 | break 62 | 63 | if len(final_body.body) > 1: 64 | break 65 | 66 | candidate_with = cst.ensure_type(candidate_with.body.body[0], cst.With) 67 | 68 | if len(compound_items) <= 1: 69 | return original_node 70 | 71 | final_body = cst.ensure_type(final_body, cst.IndentedBlock) 72 | topmost_body = cst.ensure_type(original_node.body, cst.IndentedBlock) 73 | 74 | if has_footer_comment(topmost_body) and not has_footer_comment(final_body): 75 | final_body = final_body.with_changes( 76 | footer=(*final_body.footer, *topmost_body.footer) 77 | ) 78 | 79 | return updated_node.with_changes(body=final_body, items=compound_items) 80 | 81 | 82 | __all__ = ["NestedWithTransformer"] 83 | -------------------------------------------------------------------------------- /pybetter/transformers/not_in.py: -------------------------------------------------------------------------------- 1 | import libcst as cst 2 | import libcst.matchers as m 3 | 4 | from pybetter.transformers.base import NoqaAwareTransformer 5 | 6 | 7 | class NotInConditionTransformer(NoqaAwareTransformer): 8 | @m.leave( 9 | m.UnaryOperation( 10 | operator=m.Not(), 11 | expression=m.Comparison(comparisons=[m.ComparisonTarget(operator=m.In())]), 12 | ) 13 | ) 14 | def replace_not_in_condition( 15 | self, _, updated_node: cst.UnaryOperation 16 | ) -> cst.BaseExpression: 17 | comparison_node: cst.Comparison = cst.ensure_type( 18 | updated_node.expression, cst.Comparison 19 | ) 20 | 21 | # TODO: Implement support for multiple consecutive 'not ... in B', 22 | # even if it does not make any sense in practice. 23 | return cst.Comparison( 24 | left=comparison_node.left, 25 | lpar=updated_node.lpar, 26 | rpar=updated_node.rpar, 27 | comparisons=[ 28 | comparison_node.comparisons[0].with_changes(operator=cst.NotIn()) 29 | ], 30 | ) 31 | 32 | 33 | __all__ = ["NotInConditionTransformer"] 34 | -------------------------------------------------------------------------------- /pybetter/transformers/not_is.py: -------------------------------------------------------------------------------- 1 | import libcst as cst 2 | import libcst.matchers as m 3 | 4 | from pybetter.transformers.base import NoqaAwareTransformer 5 | 6 | 7 | class NotIsConditionTransformer(NoqaAwareTransformer): 8 | @m.leave( 9 | m.UnaryOperation( 10 | operator=m.Not(), 11 | expression=m.Comparison(comparisons=[m.ComparisonTarget(operator=m.Is())]), 12 | ) 13 | ) 14 | def replace_not_in_condition( 15 | self, _, updated_node: cst.UnaryOperation 16 | ) -> cst.BaseExpression: 17 | comparison_node: cst.Comparison = cst.ensure_type( 18 | updated_node.expression, cst.Comparison 19 | ) 20 | 21 | # TODO: Implement support for multiple consecutive 'not ... in B', 22 | # even if it does not make any sense in practice. 23 | return cst.Comparison( 24 | left=comparison_node.left, 25 | lpar=updated_node.lpar, 26 | rpar=updated_node.rpar, 27 | comparisons=[ 28 | comparison_node.comparisons[0].with_changes(operator=cst.IsNot()) 29 | ], 30 | ) 31 | 32 | 33 | __all__ = ["NotIsConditionTransformer"] 34 | -------------------------------------------------------------------------------- /pybetter/transformers/parenthesized_return.py: -------------------------------------------------------------------------------- 1 | import libcst as cst 2 | import libcst.matchers as m 3 | from libcst.metadata import PositionProvider 4 | 5 | from pybetter.transformers.base import NoqaAwareTransformer 6 | 7 | 8 | class RemoveParenthesesFromReturn(NoqaAwareTransformer): 9 | @m.leave(m.Return(value=m.Tuple(lpar=m.MatchIfTrue(lambda v: v is not None)))) 10 | def remove_parentheses_from_return( 11 | self, original_node: cst.Return, updated_node: cst.Return 12 | ) -> cst.Return: 13 | # We get position of the `original_node`, since `updated_node` is 14 | # by definition different and was not processed by metadata provider. 15 | position: cst.metadata.CodeRange = self.get_metadata( 16 | PositionProvider, original_node 17 | ) 18 | 19 | # Removing parentheses which are used to enable multi-line expression 20 | # will lead to invalid code, so we do nothing. 21 | if position.start.line != position.end.line: 22 | return original_node 23 | 24 | # Removing parentheses around empty tuple does not make sense 25 | # and will not result in a correct Python expression (see issue #108) 26 | return_tuple = cst.ensure_type(updated_node.value, cst.Tuple) 27 | if len(return_tuple.elements) == 0: 28 | return original_node 29 | 30 | return updated_node.with_deep_changes(return_tuple, lpar=[], rpar=[]) 31 | 32 | 33 | __all__ = ["RemoveParenthesesFromReturn"] 34 | -------------------------------------------------------------------------------- /pybetter/transformers/unhashable_list.py: -------------------------------------------------------------------------------- 1 | from typing import List, Sequence, Union 2 | 3 | import libcst as cst 4 | from libcst import matchers as m 5 | 6 | from pybetter.transformers.base import NoqaAwareTransformer 7 | 8 | 9 | def convert_lists_to_tuples( 10 | elements: Sequence[cst.BaseElement], 11 | ) -> List[cst.BaseElement]: 12 | result: List[cst.BaseElement] = [] 13 | 14 | for element in elements: 15 | if m.matches(element, m.Element(value=m.List())): 16 | unhashable_list: cst.List = cst.ensure_type(element.value, cst.List) 17 | result.append( 18 | element.with_changes( 19 | value=cst.Tuple( 20 | elements=convert_lists_to_tuples(unhashable_list.elements) 21 | ) 22 | ) 23 | ) 24 | else: 25 | result.append(element) 26 | 27 | return result 28 | 29 | 30 | class UnhashableListTransformer(NoqaAwareTransformer): 31 | @m.call_if_inside( 32 | m.Call( 33 | func=m.OneOf(m.Name(value="set"), m.Name(value="frozenset")), 34 | args=[m.Arg(value=m.OneOf(m.List(), m.Tuple(), m.Set()))], 35 | ) 36 | | m.Set() # noqa: W503 37 | ) 38 | @m.leave(m.List() | m.Set() | m.Tuple()) 39 | def convert_list_arg( 40 | self, _, updated_node: Union[cst.Set, cst.List, cst.Tuple] 41 | ) -> cst.BaseExpression: 42 | modified_elements = convert_lists_to_tuples(updated_node.elements) 43 | return updated_node.with_changes(elements=modified_elements) 44 | 45 | 46 | __all__ = ["UnhashableListTransformer"] 47 | -------------------------------------------------------------------------------- /pybetter/utils.py: -------------------------------------------------------------------------------- 1 | import difflib 2 | import os 3 | 4 | from pygments import highlight as highlight_source 5 | 6 | from pygments.formatters.terminal256 import Terminal256Formatter 7 | from pygments.lexers.diff import DiffLexer 8 | 9 | 10 | def resolve_paths(*paths): 11 | for path in paths: 12 | if os.path.isfile(path): 13 | yield os.path.abspath(path) 14 | elif os.path.isdir(path): 15 | if path.endswith("__pycache__"): 16 | continue 17 | 18 | for dirpath, dirnames, filenames in os.walk(path): 19 | for fn in filenames: 20 | yield os.path.abspath(os.path.join(dirpath, fn)) 21 | 22 | 23 | __all__ = ["resolve_paths"] 24 | 25 | 26 | def create_diff( 27 | original_source: str, processed_source: str, source_file: str, highlight=False 28 | ) -> str: 29 | diff_text = "".join( 30 | difflib.unified_diff( 31 | original_source.splitlines(keepends=True), 32 | processed_source.splitlines(keepends=True), 33 | fromfile=source_file, 34 | tofile=source_file, 35 | ) 36 | ) 37 | 38 | if highlight: 39 | diff_text = highlight_source(diff_text, DiffLexer(), Terminal256Formatter()) 40 | 41 | return diff_text 42 | 43 | 44 | def prettify_time_interval(time_taken: float) -> str: 45 | if time_taken > 1.0: 46 | minutes, seconds = int(time_taken / 60), int(time_taken % 60) 47 | hours, minutes = int(minutes / 60), int(minutes % 60) 48 | else: 49 | # Even if it takes less than a second, precise value 50 | # may still be of interest to us. 51 | return f"{int(time_taken*1000)} milliseconds" 52 | 53 | result = [] 54 | 55 | if hours: 56 | result.append(f"{hours} hours") 57 | 58 | if minutes: 59 | result.append(f"{minutes} minutes") 60 | 61 | if seconds: 62 | result.append(f"{seconds} seconds") 63 | 64 | return " ".join(result) 65 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pybetter" 3 | version = "0.4.2" 4 | description = "Tool for fixing trivial problems with your code." 5 | authors = ["Kirill Borisov "] 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/lensvol/pybetter" 9 | keywords = ["ast"] 10 | include = [ 11 | "LICENSE", 12 | ] 13 | classifiers = [ 14 | "Environment :: Console", 15 | "Operating System :: OS Independent", 16 | "Development Status :: 3 - Alpha", 17 | "License :: OSI Approved :: MIT License", 18 | "Programming Language :: Python", 19 | "Topic :: Utilities", 20 | "Topic :: Software Development", 21 | "Intended Audience :: Developers", 22 | "Programming Language :: Python :: 3.6", 23 | "Topic :: Software Development :: Quality Assurance", 24 | ] 25 | 26 | 27 | [tool.poetry.dependencies] 28 | python = ">3.7" 29 | libcst = "^1.0.1" 30 | click = ">8.0" 31 | pyemojify = "^0.2.0" 32 | pygments = "^2.5.2" 33 | 34 | [tool.poetry.dev-dependencies] 35 | ipdb = "^0.13.2" 36 | mypy = "^1.3.0" 37 | flake8 = "^3.8.3" 38 | pytest = "^7.2.0" 39 | pytest-cov = "^4.1.0" 40 | hypothesmith = "^0.1.8" 41 | 42 | [tool.poetry.scripts] 43 | pybetter = 'pybetter.cli:main' 44 | 45 | [tool.poetry.group.dev.dependencies] 46 | codecov = "^2.1.12" 47 | 48 | [build-system] 49 | requires = ["poetry>=0.12"] 50 | build-backend = "poetry.masonry.api" 51 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = 3 | tests 4 | addopts = 5 | --cov=pybetter 6 | -v 7 | markers = 8 | wip 9 | slow -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | def check_membership(username, allowed=[], banned_sets={}): 2 | """ 3 | This function is badly written, but for once it is intentional. 4 | :param username: 5 | :param allowed:x 6 | :return: 7 | """ 8 | """One more thing""" 9 | found = True 10 | 11 | if username == None: # noqa: B003 12 | return False, f"Username not provided!" # noqa: B007 13 | 14 | for banned in banned_sets: 15 | if username in banned: 16 | return (False, rf"""User was \banned!""") 17 | 18 | if not username in allowed: 19 | found = False 20 | 21 | if found == False or found != 42 and found == True: 22 | return (False, rf"User is not \allowed!") 23 | 24 | with ctx(): 25 | with recorder() as rec: 26 | a = 42 27 | with rollback(): 28 | logging.info(f"Username {username} is logged in.") 29 | 30 | return (True, f"Hello, {username}") # noqa 31 | 32 | 33 | constant = 42 34 | _private_value = 53 35 | 36 | 37 | def main(): 38 | return check_membership("test", ["root", "another_user"], banned_sets={["hacker1"]}) 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lensvol/pybetter/fe67efea2b973f7230bf0b89bc28cdef746c2b18/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | # Shamelessly lifted from "Control skipping of tests according to command 5 | # line option" section of Pytest manual. 6 | 7 | 8 | def pytest_addoption(parser): 9 | parser.addoption( 10 | "--run-slow", action="store_true", default=False, help="run slow tests" 11 | ) 12 | 13 | 14 | def pytest_configure(config): 15 | config.addinivalue_line("markers", "slow: mark test as slow to run") 16 | 17 | 18 | def pytest_collection_modifyitems(config, items): 19 | if config.getoption("--run-slow"): 20 | # --run-slow given in cli: do not skip slow tests 21 | return 22 | skip_slow = pytest.mark.skip(reason="need --run-slow option to run") 23 | for item in items: 24 | if "slow" in item.keywords: 25 | item.add_marker(skip_slow) 26 | -------------------------------------------------------------------------------- /tests/test_all_attribute.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pybetter.cli import process_file 4 | from pybetter.improvements import FixMissingAllAttribute 5 | 6 | NO_CHANGES_MADE = None 7 | 8 | TRIVIAL_CASE_ONLY_GLOBALS = ( 9 | """ 10 | def f(): 11 | pass 12 | 13 | class A: 14 | pass 15 | 16 | abc = None 17 | """, 18 | """ 19 | def f(): 20 | pass 21 | 22 | class A: 23 | pass 24 | 25 | abc = None 26 | 27 | 28 | __all__ = [ 29 | 'A', 30 | 'abc', 31 | 'f' 32 | ] 33 | """, 34 | ) 35 | 36 | 37 | NESTED_DEFINITIONS_ARE_IGNORED = ( 38 | """ 39 | def f(): 40 | def inner(): 41 | pass 42 | 43 | class A: 44 | def method(self): 45 | self.inner_abc = 42 46 | 47 | 48 | abc = None 49 | """, 50 | """ 51 | def f(): 52 | def inner(): 53 | pass 54 | 55 | class A: 56 | def method(self): 57 | self.inner_abc = 42 58 | 59 | 60 | abc = None 61 | 62 | 63 | __all__ = [ 64 | 'A', 65 | 'abc', 66 | 'f' 67 | ] 68 | """, 69 | ) 70 | 71 | PRIVATE_DEFINITIONS_ARE_IGNORED = ( 72 | """ 73 | def _private_func(): 74 | pass 75 | 76 | class _A: 77 | pass 78 | 79 | _abc = None 80 | 81 | def public(): 82 | pass 83 | """, 84 | """ 85 | def _private_func(): 86 | pass 87 | 88 | class _A: 89 | pass 90 | 91 | _abc = None 92 | 93 | def public(): 94 | pass 95 | 96 | 97 | __all__ = [ 98 | 'public' 99 | ] 100 | """, 101 | ) 102 | 103 | EXISTING_ALL_IS_UNCHANGED = ( 104 | """ 105 | def func(): 106 | pass 107 | 108 | class A: 109 | pass 110 | 111 | abc = None 112 | 113 | __all__ = ["A"] 114 | """, 115 | NO_CHANGES_MADE, 116 | ) 117 | 118 | EMPTY_ALL_IS_NOT_GENERATED = ( 119 | """ 120 | def _func(): 121 | pass 122 | 123 | class _A: 124 | pass 125 | 126 | _abc = None 127 | """, 128 | NO_CHANGES_MADE, 129 | ) 130 | 131 | DUPLICATES_REMOVED = ( 132 | """ 133 | from typing import overload 134 | 135 | @overload 136 | def a(v: int) -> int: ... 137 | 138 | @overload 139 | def a(v: str) -> str: ... 140 | 141 | def b() -> int: ... 142 | 143 | def a(v: int | str ) -> int | str: ... 144 | """, 145 | """ 146 | from typing import overload 147 | 148 | @overload 149 | def a(v: int) -> int: ... 150 | 151 | @overload 152 | def a(v: str) -> str: ... 153 | 154 | def b() -> int: ... 155 | 156 | def a(v: int | str ) -> int | str: ... 157 | 158 | 159 | __all__ = [ 160 | 'a', 161 | 'b' 162 | ] 163 | """, 164 | ) 165 | 166 | 167 | @pytest.mark.parametrize( 168 | "original,expected", 169 | [ 170 | TRIVIAL_CASE_ONLY_GLOBALS, 171 | NESTED_DEFINITIONS_ARE_IGNORED, 172 | PRIVATE_DEFINITIONS_ARE_IGNORED, 173 | EXISTING_ALL_IS_UNCHANGED, 174 | EMPTY_ALL_IS_NOT_GENERATED, 175 | DUPLICATES_REMOVED, 176 | ], 177 | ids=[ 178 | "trivial case", 179 | "nested definitions are ignored", 180 | "private definitions are ignored", 181 | "existing __all__ is unchanged", 182 | "empty __all__ is not generated", 183 | "duplicates removed", 184 | ], 185 | ) 186 | def test_generation_of_dunder_all(original, expected): 187 | processed, _ = process_file(original.strip(), [FixMissingAllAttribute]) 188 | 189 | assert processed.strip() == (expected or original).strip() 190 | -------------------------------------------------------------------------------- /tests/test_boolean_equality.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pybetter.cli import process_file 4 | from pybetter.improvements import FixBooleanEqualityChecks 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "original,expected", 9 | [ 10 | ("a == False", "not a"), 11 | ("a == True", "a"), 12 | ("a == b < 42 == True", "a == b < 42"), 13 | ("(a == True) == False", "not (a == True)"), 14 | ], 15 | ids=[ 16 | "comparison with False", 17 | "comparison with True", 18 | "several comparisons", 19 | "nested comparisons", 20 | ], 21 | ) 22 | def test_boolean_equality_fix(original, expected): 23 | processed, _ = process_file(original, [FixBooleanEqualityChecks]) 24 | 25 | assert processed == expected 26 | -------------------------------------------------------------------------------- /tests/test_equals_none.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pybetter.cli import process_file 4 | from pybetter.improvements import FixEqualsNone 5 | 6 | NO_CHANGES_MADE = None 7 | 8 | TRIVIAL_NONE_COMPARISON = ( 9 | """ 10 | if a == None: 11 | pass 12 | """, 13 | """ 14 | if a is None: 15 | pass 16 | """, 17 | ) 18 | 19 | 20 | NONE_IDENTITY_CHECK_IGNORED = ( 21 | """ 22 | if a is None: 23 | pass 24 | """, 25 | NO_CHANGES_MADE, 26 | ) 27 | 28 | 29 | NESTED_NONE_CMPS_PROCESSED = ( 30 | """ 31 | if a == None == None == None: 32 | pass 33 | """, 34 | """ 35 | if a is None is None is None: 36 | pass 37 | """, 38 | ) 39 | 40 | 41 | @pytest.mark.parametrize( 42 | "original,expected", 43 | [TRIVIAL_NONE_COMPARISON, NONE_IDENTITY_CHECK_IGNORED, NESTED_NONE_CMPS_PROCESSED], 44 | ids=[ 45 | "trivial comparison with None", 46 | "identity check with None", 47 | "multiple comparisons with None", 48 | ], 49 | ) 50 | def test_trivial_fmt_string_conversion(original, expected): 51 | processed, _ = process_file(original.strip(), [FixEqualsNone]) 52 | 53 | assert processed.strip() == (expected or original).strip() 54 | -------------------------------------------------------------------------------- /tests/test_mutable_args.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pybetter.cli import process_file 4 | from pybetter.improvements import FixMutableDefaultArgs 5 | 6 | NO_CHANGES_MADE = None 7 | 8 | # In these samples we do not use indents for formatting, 9 | # since this transformer uses module's inferred indentation 10 | # settings, and those will be like _8_ spaces or such. 11 | NON_MUTABLE_DEFAULTS_IGNORED = ( 12 | """ 13 | def f(a=None, b=frozenset(), c=42): 14 | pass 15 | """, 16 | NO_CHANGES_MADE, 17 | ) 18 | 19 | EMPTY_MUTABLE_DEFAULT_EXTRACTED = ( 20 | """ 21 | def f(a=[]): 22 | pass 23 | """, 24 | """ 25 | def f(a=None): 26 | 27 | if a is None: 28 | a = [] 29 | 30 | pass 31 | """, 32 | ) 33 | 34 | NONEMPTY_MUTABLE_DEFAULT_EXTRACTED = ( 35 | """ 36 | def f(a=[42]): 37 | pass 38 | """, 39 | """ 40 | def f(a=None): 41 | 42 | if a is None: 43 | a = [42] 44 | 45 | pass 46 | """, 47 | ) 48 | 49 | NESTED_FUNCTIONS_ARE_PROCESSED = ( 50 | """ 51 | def outer(a=[53]): 52 | def inner(b=[42]): 53 | pass 54 | """, 55 | """ 56 | def outer(a=None): 57 | 58 | if a is None: 59 | a = [53] 60 | 61 | def inner(b=None): 62 | 63 | if b is None: 64 | b = [42] 65 | 66 | pass 67 | """, 68 | ) 69 | 70 | 71 | ARGUMENT_ORDER_PRESERVED = ( 72 | """ 73 | def outer(b=[], a={}): 74 | pass 75 | """, 76 | """ 77 | def outer(b=None, a=None): 78 | 79 | if b is None: 80 | b = [] 81 | 82 | if a is None: 83 | a = {} 84 | 85 | pass 86 | """, 87 | ) 88 | 89 | 90 | @pytest.mark.parametrize( 91 | "original,expected", 92 | [ 93 | NON_MUTABLE_DEFAULTS_IGNORED, 94 | EMPTY_MUTABLE_DEFAULT_EXTRACTED, 95 | NONEMPTY_MUTABLE_DEFAULT_EXTRACTED, 96 | NESTED_FUNCTIONS_ARE_PROCESSED, 97 | ARGUMENT_ORDER_PRESERVED, 98 | ], 99 | ids=[ 100 | "non-mutable defaults are ignored", 101 | "empty mutable default is extracted", 102 | "non-empty mutable default", 103 | "nested functions with defaults", 104 | "argument order is preserved", 105 | ], 106 | ) 107 | def test_mutable_defaults_extraction(original, expected): 108 | processed, _ = process_file(original.strip(), [FixMutableDefaultArgs]) 109 | assert processed.strip() == (expected or original).strip() 110 | -------------------------------------------------------------------------------- /tests/test_nested_withs.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pybetter.cli import process_file 4 | from pybetter.improvements import FixTrivialNestedWiths 5 | 6 | 7 | NO_CHANGES_MADE = None 8 | 9 | TRIVIAL_NESTED_WITH = ( 10 | """ 11 | with a(): 12 | with b(): 13 | print("Hello, world!") 14 | """, 15 | """ 16 | with a(), b(): 17 | print("Hello, world!") 18 | """, 19 | ) 20 | 21 | MULTIPLE_LEVEL_NESTED_WITH = ( 22 | """ 23 | with a(): 24 | with b(): 25 | with c(): 26 | print("Hello, world!") 27 | """, 28 | """ 29 | with a(), b(), c(): 30 | print("Hello, world!") 31 | """, 32 | ) 33 | 34 | STATEMENTS_BETWEEN_WITHS = ( 35 | """ 36 | with a(): 37 | constant = 42 38 | with b(): 39 | print("Hello, world!") 40 | """, 41 | NO_CHANGES_MADE, 42 | ) 43 | 44 | INLINE_COMMENT_PREVENTS_COLLAPSE = ( 45 | """ 46 | with a(): # TODO: something 47 | with b(): 48 | print("Hello, world!") 49 | """, 50 | NO_CHANGES_MADE, 51 | ) 52 | 53 | LEADING_COMMENT_PREVENTS_COLLAPSE = ( 54 | """ 55 | with a(): 56 | # TODO: something 57 | with b(): 58 | print("Hello, world!") 59 | """, 60 | NO_CHANGES_MADE, 61 | ) 62 | 63 | COMMENT_IN_OUTERMOST_FOOTER_IS_PRESERVED = ( 64 | """ 65 | with a(): 66 | with b(): 67 | print("Hello, world!") 68 | 69 | # Roses are red, 70 | # Violets are blue, 71 | # Sugar is sweet, 72 | # And so are you. 73 | """, 74 | """ 75 | with a(), b(): 76 | print("Hello, world!") 77 | 78 | # Roses are red, 79 | # Violets are blue, 80 | # Sugar is sweet, 81 | # And so are you. 82 | """, 83 | ) 84 | 85 | SINGLE_LINE_BODY_IS_PRESERVED = ( 86 | """ 87 | with a(): 88 | with b(): 89 | import pdb; pdb.set_trace() 90 | """, 91 | """ 92 | with a(), b(): 93 | import pdb; pdb.set_trace() 94 | """, 95 | ) 96 | 97 | ALIASES_ARE_PRESERVED = ( 98 | """ 99 | with a() as another_a: 100 | with b() as other_b: 101 | another_a.call() 102 | other_b.call() 103 | """, 104 | """ 105 | with a() as another_a, b() as other_b: 106 | another_a.call() 107 | other_b.call() 108 | """, 109 | ) 110 | 111 | UNRELATED_CODE_IS_NOT_PROCESSED = ( 112 | """ 113 | a = 2 114 | b = 3 115 | assert a + b == 5 116 | """, 117 | NO_CHANGES_MADE, 118 | ) 119 | 120 | SINGLE_WITH_IS_LEFT_ALONE = ( 121 | """ 122 | with logger(): 123 | print("Hello, world!") 124 | """, 125 | NO_CHANGES_MADE, 126 | ) 127 | 128 | 129 | ASYNC_WITH_IS_LEFT_ALONE = ( 130 | """ 131 | with open(fn, "w") as fob: 132 | async with make_request() as resp: 133 | pass 134 | """, 135 | NO_CHANGES_MADE, 136 | ) 137 | 138 | ASYNC_IN_THE_MIDDLE_PREVENTS_COMPOUNDING = ( 139 | """ 140 | with a(): 141 | async with abc(): 142 | with b(): 143 | pass 144 | """, 145 | NO_CHANGES_MADE, 146 | ) 147 | 148 | 149 | @pytest.mark.parametrize( 150 | "original,expected", 151 | [ 152 | TRIVIAL_NESTED_WITH, 153 | MULTIPLE_LEVEL_NESTED_WITH, 154 | STATEMENTS_BETWEEN_WITHS, 155 | INLINE_COMMENT_PREVENTS_COLLAPSE, 156 | LEADING_COMMENT_PREVENTS_COLLAPSE, 157 | COMMENT_IN_OUTERMOST_FOOTER_IS_PRESERVED, 158 | SINGLE_LINE_BODY_IS_PRESERVED, 159 | UNRELATED_CODE_IS_NOT_PROCESSED, 160 | SINGLE_WITH_IS_LEFT_ALONE, 161 | ALIASES_ARE_PRESERVED, 162 | ASYNC_WITH_IS_LEFT_ALONE, 163 | ASYNC_IN_THE_MIDDLE_PREVENTS_COMPOUNDING, 164 | ], 165 | ids=[ 166 | "trivial nested 'with'", 167 | "multiple nested 'with's", 168 | "statements between 'with's", 169 | "inline comment prevents collapse", 170 | "leading comment prevents collapse", 171 | "outermost comment is preserved", 172 | "single line body is preserved", 173 | "unrelated code", 174 | "single 'with' is left alone", 175 | "'with' aliases are preserved", 176 | "'async with' is left alone", 177 | "'async' prevents compounding of 'with's", 178 | ], 179 | ) 180 | def test_collapse_of_nested_with_statements(original, expected): 181 | processed, _ = process_file(original.strip(), [FixTrivialNestedWiths]) 182 | 183 | assert processed.strip() == (expected or original).strip() 184 | -------------------------------------------------------------------------------- /tests/test_no_crashes.py: -------------------------------------------------------------------------------- 1 | import hypothesmith 2 | import pytest 3 | from hypothesis import given, settings, HealthCheck 4 | 5 | from pybetter.cli import process_file, ALL_IMPROVEMENTS 6 | 7 | 8 | settings.register_profile( 9 | "slow_example_generation", 10 | suppress_health_check=list(HealthCheck), 11 | ) 12 | settings.load_profile("slow_example_generation") 13 | 14 | 15 | @pytest.mark.slow 16 | @given(generated_source=hypothesmith.from_grammar()) 17 | def test_no_crashes_on_valid_input(generated_source): 18 | process_file(generated_source, ALL_IMPROVEMENTS) 19 | -------------------------------------------------------------------------------- /tests/test_not_in.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pybetter.cli import process_file 4 | from pybetter.improvements import FixEqualsNone, FixNotInConditionOrder 5 | 6 | NO_CHANGES_MADE = None 7 | 8 | TRIVIAL_NOT_A_IN_B_CASE = ( 9 | """ 10 | if not a in b: 11 | pass 12 | """, 13 | """ 14 | if a not in b: 15 | pass 16 | """, 17 | ) 18 | 19 | PARENS_NOT_A_IN_B_CASE = ( 20 | """ 21 | if not (a in b): 22 | pass 23 | """, 24 | """ 25 | if a not in b: 26 | pass 27 | """, 28 | ) 29 | 30 | NESTED_COMPARISONS = ( 31 | """ 32 | if not ((not a in b) in d): 33 | pass 34 | """, 35 | """ 36 | if (a not in b) not in d: 37 | pass 38 | """, 39 | ) 40 | 41 | DIFFERENT_COMPARISONS_IN_ROW = ( 42 | """ 43 | if not (a in b and a > 42): 44 | pass 45 | """, 46 | NO_CHANGES_MADE, 47 | ) 48 | 49 | 50 | CONSECUTIVE_COMPARISONS_UNCHANGED = ( 51 | """ 52 | if not (a in b in c in d): 53 | pass 54 | """, 55 | NO_CHANGES_MADE, 56 | ) 57 | 58 | 59 | @pytest.mark.parametrize( 60 | "original,expected", 61 | [ 62 | TRIVIAL_NOT_A_IN_B_CASE, 63 | PARENS_NOT_A_IN_B_CASE, 64 | NESTED_COMPARISONS, 65 | DIFFERENT_COMPARISONS_IN_ROW, 66 | CONSECUTIVE_COMPARISONS_UNCHANGED, 67 | ], 68 | ids=[ 69 | "trivial 'not A in B'", 70 | "parenthesized 'not A in B'", 71 | "nested comparisons", 72 | "several types of comparisons", 73 | "consecutive comparisons", 74 | ], 75 | ) 76 | def test_not_in_transformation(original, expected): 77 | processed, _ = process_file(original.strip(), [FixNotInConditionOrder]) 78 | 79 | assert processed.strip() == (expected or original).strip() 80 | -------------------------------------------------------------------------------- /tests/test_not_is.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pybetter.cli import process_file 4 | from pybetter.improvements import FixNotIsConditionOrder 5 | 6 | NO_CHANGES_MADE = None 7 | 8 | TRIVIAL_NOT_A_IS_B_CASE = ( 9 | """ 10 | if not a is B: 11 | pass 12 | """, 13 | """ 14 | if a is not B: 15 | pass 16 | """, 17 | ) 18 | 19 | 20 | MULTIPLE_NOT_A_IS_B_CASE = ( 21 | """ 22 | if foo is not None and not bar is None: 23 | pass 24 | """, 25 | """ 26 | if foo is not None and bar is not None: 27 | pass 28 | """, 29 | ) 30 | 31 | 32 | NESTED_NOT_A_IS_B_CASE = ( 33 | """ 34 | if not foo is (not A is B): 35 | pass 36 | """, 37 | """ 38 | if foo is not (A is not B): 39 | pass 40 | """, 41 | ) 42 | 43 | 44 | @pytest.mark.parametrize( 45 | "original,expected", 46 | [ 47 | TRIVIAL_NOT_A_IS_B_CASE, 48 | MULTIPLE_NOT_A_IS_B_CASE, 49 | NESTED_NOT_A_IS_B_CASE, 50 | ], 51 | ids=[ 52 | "trivial 'not A is B' case", 53 | "multiple 'not A is B' in same expression", 54 | "nested 'not A is B' case", 55 | ], 56 | ) 57 | def test_not_in_transformation(original, expected): 58 | processed, _ = process_file(original.strip(), [FixNotIsConditionOrder]) 59 | 60 | assert processed.strip() == (expected or original).strip() 61 | -------------------------------------------------------------------------------- /tests/test_parenthesized_return.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pybetter.cli import process_file 4 | from pybetter.improvements import FixParenthesesInReturn 5 | 6 | 7 | NO_CHANGES_MADE = None 8 | 9 | TRIVIAL_RETURNED_TUPLE = ( 10 | """ 11 | def f(): 12 | return (42,) 13 | """, 14 | """ 15 | def f(): 16 | return 42, 17 | """, 18 | ) 19 | 20 | MULTIPLE_ELEMENT_TUPLE = ( 21 | """ 22 | def f(): 23 | return (True, 'User is not welcome') 24 | """, 25 | """ 26 | def f(): 27 | return True, 'User is not welcome' 28 | """, 29 | ) 30 | 31 | MULTILINE_RETURN_TUPLE = ( 32 | """ 33 | def f(): 34 | return ( 35 | True, 36 | "User is not welcome" 37 | ) 38 | """, 39 | NO_CHANGES_MADE, 40 | ) 41 | 42 | 43 | TUPLE_INSIDE_RETURN_EXPR = ( 44 | """ 45 | def f(): 46 | return {"42", ("abcdef",)} 47 | """, 48 | NO_CHANGES_MADE, 49 | ) 50 | 51 | 52 | EMPTY_TUPLE_RETURN_EXPR = ( 53 | """ 54 | def foo() -> typing.Tuple: 55 | return () 56 | """, 57 | NO_CHANGES_MADE, 58 | ) 59 | 60 | 61 | @pytest.mark.parametrize( 62 | "original,expected", 63 | [ 64 | TRIVIAL_RETURNED_TUPLE, 65 | MULTIPLE_ELEMENT_TUPLE, 66 | MULTILINE_RETURN_TUPLE, 67 | TUPLE_INSIDE_RETURN_EXPR, 68 | EMPTY_TUPLE_RETURN_EXPR, 69 | ], 70 | ids=[ 71 | "trivial returned tuple", 72 | "multiple elements in returned tuple", 73 | "multi-line returns not processed", 74 | "tuple inside returned expression", 75 | "function returns empty tuple", 76 | ], 77 | ) 78 | def test_removal_of_parentheses_in_return(original, expected): 79 | processed, _ = process_file(original.strip(), [FixParenthesesInReturn]) 80 | 81 | assert processed.strip() == (expected or original).strip() 82 | -------------------------------------------------------------------------------- /tests/test_trivial_fstring.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pybetter.cli import process_file 4 | from pybetter.improvements import FixTrivialFmtStringCreation 5 | 6 | NO_CHANGES_MADE = None 7 | 8 | TRIVIAL_FSTRING = ( 9 | """ 10 | f"Hello, world!" 11 | """, 12 | """ 13 | "Hello, world!" 14 | """, 15 | ) 16 | 17 | 18 | TRIVIAL_FSTRING_IN_TRIPLE_QUOTES = ( 19 | ''' 20 | f"""Hello, world!""" 21 | ''', 22 | ''' 23 | """Hello, world!""" 24 | ''', 25 | ) 26 | 27 | TRIVIAL_FSTRING_TRIPLE_QUOTES_R_PREFIX_AFTER = ( 28 | ''' 29 | fr"""Hello,\bworld!""" 30 | ''', 31 | ''' 32 | r"""Hello,\bworld!""" 33 | ''', 34 | ) 35 | 36 | TRIVIAL_FSTRING_TRIPLE_QUOTES_R_PREFIX_BEFORE = ( 37 | ''' 38 | rf"""Hello,\bworld!""" 39 | ''', 40 | ''' 41 | r"""Hello,\bworld!""" 42 | ''', 43 | ) 44 | 45 | FSTRING_WITH_ARGUMENTS = ( 46 | """ 47 | f"Hello, {username}" 48 | """, 49 | NO_CHANGES_MADE, 50 | ) 51 | 52 | EMPTY_FSTRING = ( 53 | """ 54 | f"" 55 | """, 56 | NO_CHANGES_MADE, 57 | ) 58 | 59 | 60 | @pytest.mark.wip 61 | @pytest.mark.parametrize( 62 | "original,expected", 63 | [ 64 | TRIVIAL_FSTRING, 65 | TRIVIAL_FSTRING_IN_TRIPLE_QUOTES, 66 | TRIVIAL_FSTRING_TRIPLE_QUOTES_R_PREFIX_BEFORE, 67 | TRIVIAL_FSTRING_TRIPLE_QUOTES_R_PREFIX_AFTER, 68 | FSTRING_WITH_ARGUMENTS, 69 | EMPTY_FSTRING, 70 | ], 71 | ids=[ 72 | "trivial f-string", 73 | "trivial f-string in triple quotes", 74 | "trivial f-string in triple quotes with 'rf' prefix", 75 | "trivial f-string in triple quotes with 'fr' prefix", 76 | "f-string with arguments", 77 | "empty f-string", 78 | ], 79 | ) 80 | def test_trivial_fmt_string_conversion(original, expected): 81 | processed, _ = process_file(original.strip(), [FixTrivialFmtStringCreation]) 82 | 83 | assert processed.strip() == (expected or original).strip() 84 | -------------------------------------------------------------------------------- /tests/test_unhashable_list.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pybetter.cli import process_file 4 | from pybetter.improvements import FixUnhashableList 5 | 6 | NO_CHANGES_MADE = None 7 | 8 | TRIVIAL_LIST_LITERAL = ( 9 | """ 10 | {[1, 2]} 11 | """, 12 | """ 13 | {(1, 2)} 14 | """, 15 | ) 16 | 17 | 18 | SET_FUNCTION_LIST_ARGUMENT = ( 19 | """ 20 | set([["a", "b"], "c"]) 21 | """, 22 | """ 23 | set([("a", "b"), "c"]) 24 | """, 25 | ) 26 | 27 | 28 | FROZENSET_FUNCTION_LIST_ARGUMENT = ( 29 | """ 30 | frozenset([["a", "b"], "c"]) 31 | """, 32 | """ 33 | frozenset([("a", "b"), "c"]) 34 | """, 35 | ) 36 | 37 | 38 | SET_FUNCTION_SET_ARGUMENT = ( 39 | """ 40 | set({["a", "b"], "c"}) 41 | """, 42 | """ 43 | set({("a", "b"), "c"}) 44 | """, 45 | ) 46 | 47 | 48 | FROZENSET_FUNCTION_SET_ARGUMENT = ( 49 | """ 50 | frozenset({["a", "b"], "c"}) 51 | """, 52 | """ 53 | frozenset({("a", "b"), "c"}) 54 | """, 55 | ) 56 | 57 | SET_FUNCTION_TUPLE_ARGUMENT = ( 58 | """ 59 | set((["a", "b"], "c")) 60 | """, 61 | """ 62 | set((("a", "b"), "c")) 63 | """, 64 | ) 65 | 66 | 67 | FROZENSET_FUNCTION_TUPLE_ARGUMENT = ( 68 | """ 69 | frozenset((["a", "b"], "c")) 70 | """, 71 | """ 72 | frozenset((("a", "b"), "c")) 73 | """, 74 | ) 75 | 76 | NESTED_LIST_LITERAL = ( 77 | """ 78 | {[["a", "b"], 2]} 79 | """, 80 | """ 81 | {(("a", "b"), 2)} 82 | """, 83 | ) 84 | 85 | TUPLES_ARE_UNCHANGED = ( 86 | """ 87 | {(1, 2), "a"} 88 | """, 89 | NO_CHANGES_MADE, 90 | ) 91 | 92 | 93 | REGULAR_LIST_ARGUMENTS_ARE_UNCHANGED = ( 94 | """ 95 | {func([1, 2, 3])} 96 | """, 97 | NO_CHANGES_MADE, 98 | ) 99 | 100 | 101 | COMPLEX_LIST_ARGUMENTS_ARE_UNCHANGED = ( 102 | """ 103 | {frozenset(set([func([1, 2, 3])]))} 104 | """, 105 | NO_CHANGES_MADE, 106 | ) 107 | 108 | 109 | NESTED_SETS_ARE_PROCESSED = ( 110 | """ 111 | { 112 | {[1, 2]}, 113 | { 114 | set([[1, 2], ["a", "b"]]) 115 | }, 116 | } 117 | """, 118 | """ 119 | { 120 | {(1, 2)}, 121 | { 122 | set([(1, 2), ("a", "b")]) 123 | }, 124 | } 125 | """, 126 | ) 127 | 128 | 129 | @pytest.mark.parametrize( 130 | "original,expected", 131 | [ 132 | TRIVIAL_LIST_LITERAL, 133 | SET_FUNCTION_LIST_ARGUMENT, 134 | SET_FUNCTION_SET_ARGUMENT, 135 | SET_FUNCTION_TUPLE_ARGUMENT, 136 | FROZENSET_FUNCTION_LIST_ARGUMENT, 137 | FROZENSET_FUNCTION_SET_ARGUMENT, 138 | FROZENSET_FUNCTION_TUPLE_ARGUMENT, 139 | NESTED_LIST_LITERAL, 140 | TUPLES_ARE_UNCHANGED, 141 | NESTED_SETS_ARE_PROCESSED, 142 | COMPLEX_LIST_ARGUMENTS_ARE_UNCHANGED, 143 | ], 144 | ids=[ 145 | "list literal in {}", 146 | "list argument to set()", 147 | "set argument to set()", 148 | "tuple argument to set()", 149 | "list argument to frozenset()", 150 | "set argument to frozenset()", 151 | "tuple argument to frozenset()", 152 | "nested list literal", 153 | "tuples are changed", 154 | "nested sets", 155 | "comples list arguments are unchanged", 156 | ], 157 | ) 158 | def test_replacement_of_list_literal_in_sets(original, expected): 159 | processed, _ = process_file(original.strip(), [FixUnhashableList]) 160 | 161 | assert processed.strip() == (expected or original).strip() 162 | --------------------------------------------------------------------------------