pax_global_header00006660000000000000000000000064144411445110014511gustar00rootroot0000000000000052 comment=f280ad4585de0e39d5ae81ecdc49f65f1309bfda flake8-2020-1.8.1/000077500000000000000000000000001444114451100132535ustar00rootroot00000000000000flake8-2020-1.8.1/.github/000077500000000000000000000000001444114451100146135ustar00rootroot00000000000000flake8-2020-1.8.1/.github/workflows/000077500000000000000000000000001444114451100166505ustar00rootroot00000000000000flake8-2020-1.8.1/.github/workflows/main.yml000066400000000000000000000003241444114451100203160ustar00rootroot00000000000000name: main on: push: branches: [main, test-me-*] tags: '*' pull_request: jobs: main: uses: asottile/workflows/.github/workflows/tox.yml@v1.0.0 with: env: '["py38", "py39", "py310"]' flake8-2020-1.8.1/.gitignore000066400000000000000000000000421444114451100152370ustar00rootroot00000000000000*.egg-info *.pyc /.coverage /.tox flake8-2020-1.8.1/.pre-commit-config.yaml000066400000000000000000000022231444114451100175330ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: debug-statements - id: double-quote-string-fixer - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt rev: v2.3.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports rev: v3.9.0 hooks: - id: reorder-python-imports args: [--py38-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v2.4.0 hooks: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade rev: v3.4.0 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/pre-commit/mirrors-autopep8 rev: v2.0.2 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.3.0 hooks: - id: mypy flake8-2020-1.8.1/LICENSE000066400000000000000000000020431444114451100142570ustar00rootroot00000000000000Copyright (c) 2019 Anthony Sottile Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. flake8-2020-1.8.1/README.md000066400000000000000000000071471444114451100145430ustar00rootroot00000000000000[![build status](https://github.com/asottile/flake8-2020/actions/workflows/main.yml/badge.svg)](https://github.com/asottile/flake8-2020/actions/workflows/main.yml) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/asottile/flake8-2020/main.svg)](https://results.pre-commit.ci/latest/github/asottile/flake8-2020/main) flake8-2020 =========== flake8 plugin which checks for misuse of `sys.version` or `sys.version_info` this will become a problem when `python3.10` or `python4.0` exists (presumably during the year 2020). you might also find an early build of [python3.10] useful [python3.10]: https://github.com/asottile/python3.10 ## installation ```bash pip install flake8-2020 ``` ## flake8 codes | Code | Description | |--------|--------------------------------------------------------| | YTT101 | `sys.version[:3]` referenced (python3.10) | | YTT102 | `sys.version[2]` referenced (python3.10) | | YTT103 | `sys.version` compared to string (python3.10) | | YTT201 | `sys.version_info[0] == 3` referenced (python4) | | YTT202 | `six.PY3` referenced (python4) | | YTT203 | `sys.version_info[1]` compared to integer (python4) | | YTT204 | `sys.version_info.minor` compared to integer (python4) | | YTT301 | `sys.version[0]` referenced (python10) | | YTT302 | `sys.version` compared to string (python10) | | YTT303 | `sys.version[:1]` referenced (python10) | ## rationale lots of code incorrectly references the `sys.version` and `sys.version_info` members. in particular, this will cause some issues when the version of python after python3.9 is released. my current recommendation is 3.10 since I believe it breaks less code, here's a few patterns that will cause issues: ```python # in python3.10 this will report as '3.1' (should be '3.10') python_version = sys.version[:3] # YTT101 # in python3.10 this will report as '1' (should be '10') py_minor = sys.version[2] # in python3.10 this will be False (which goes against developer intention) sys.version >= '3.5' # YTT103 # correct way to do this python_version = '{}.{}'.format(*sys.version_info) py_minor = str(sys.version_info[1]) sys.version_info >= (3, 5) ``` ```python # in python4 this will report as `False` (and suddenly run python2 code!) is_py3 = sys.version_info[0] == 3 # YTT201 # in python4 this will report as `False` (six violates YTT201!) if six.PY3: # YTT202 print('python3!') if sys.version_info[0] >= 3 and sys.version_info[1] >= 5: # YTT203 print('py35+') if sys.version_info.major >= 3 and sys.version_info.minor >= 6: # YTT204 print('py36+') # correct way to do this is_py3 = sys.version_info >= (3,) if not six.PY2: print('python3!') if sys.version_info >= (3, 5): print('py35+') if sys.version_info >= (3, 6): print('py36+') ``` ```python # in python10 this will report as '1' python_major_version = sys.version[0] # YTT301 # in python10 this will be False if sys.version >= '3': # YTT302 print('python3!') # in python10 this will be False if sys.version[:1] >= '3': # YTT303 print('python3!') # correct way to do this python_major_version = str(sys.version_info[0]) if sys.version_info >= (3,): print('python3!') if sys.version_info >= (3,): print('python3!') ``` ## as a pre-commit hook See [pre-commit](https://github.com/pre-commit/pre-commit) for instructions Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/pycqa/flake8 rev: 3.7.8 hooks: - id: flake8 additional_dependencies: [flake8-2020==1.6.1] ``` flake8-2020-1.8.1/flake8_2020.py000066400000000000000000000150011444114451100154370ustar00rootroot00000000000000from __future__ import annotations import ast import sys from typing import Any from typing import Generator YTT101 = 'YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`' # noqa: E501 YTT102 = 'YTT102 `sys.version[2]` referenced (python3.10), use `sys.version_info`' # noqa: E501 YTT103 = 'YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`' # noqa: E501 YTT201 = 'YTT201 `sys.version_info[0] == 3` referenced (python4), use `>=`' YTT202 = 'YTT202 `six.PY3` referenced (python4), use `not six.PY2`' YTT203 = 'YTT203 `sys.version_info[1]` compared to integer (python4), compare `sys.version_info` to tuple' # noqa: E501 YTT204 = 'YTT204 `sys.version_info.minor` compared to integer (python4), compare `sys.version_info` to tuple' # noqa: E501 YTT301 = 'YTT301 `sys.version[0]` referenced (python10), use `sys.version_info`' # noqa: E501 YTT302 = 'YTT302 `sys.version` compared to string (python10), use `sys.version_info`' # noqa: E501 YTT303 = 'YTT303 `sys.version[:1]` referenced (python10), use `sys.version_info`' # noqa: E501 def _is_index(node: ast.Subscript, n: int) -> bool: if sys.version_info >= (3, 9): # pragma: >=3.9 cover node_slice = node.slice elif isinstance(node.slice, ast.Index): # pragma: <3.9 cover node_slice = node.slice.value else: # pragma: <3.9 cover return False return isinstance(node_slice, ast.Constant) and node_slice.value == n class Visitor(ast.NodeVisitor): def __init__(self) -> None: self.errors: list[tuple[int, int, str]] = [] self._from_imports: dict[str, str] = {} def visit_ImportFrom(self, node: ast.ImportFrom) -> None: for alias in node.names: if node.module is not None and not alias.asname: self._from_imports[alias.name] = node.module self.generic_visit(node) def _is_sys(self, attr: str, node: ast.AST) -> bool: return ( # subscripting `sys.` isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) and node.value.id == 'sys' and node.attr == attr ) or ( isinstance(node, ast.Name) and node.id == attr and self._from_imports.get(node.id) == 'sys' ) def _is_sys_version_upper_slice(self, node: ast.Subscript, n: int) -> bool: return ( self._is_sys('version', node.value) and isinstance(node.slice, ast.Slice) and node.slice.lower is None and isinstance(node.slice.upper, ast.Constant) and node.slice.upper.value == n and node.slice.step is None ) def visit_Subscript(self, node: ast.Subscript) -> None: if self._is_sys_version_upper_slice(node, 1): self.errors.append(( node.value.lineno, node.value.col_offset, YTT303, )) elif self._is_sys_version_upper_slice(node, 3): self.errors.append(( node.value.lineno, node.value.col_offset, YTT101, )) elif self._is_sys('version', node.value) and _is_index(node, n=2): self.errors.append(( node.value.lineno, node.value.col_offset, YTT102, )) elif self._is_sys('version', node.value) and _is_index(node, n=0): self.errors.append(( node.value.lineno, node.value.col_offset, YTT301, )) self.generic_visit(node) def visit_Compare(self, node: ast.Compare) -> None: if ( isinstance(node.left, ast.Subscript) and self._is_sys('version_info', node.left.value) and _is_index(node.left, n=0) and len(node.ops) == 1 and isinstance(node.ops[0], (ast.Eq, ast.NotEq)) and isinstance(node.comparators[0], ast.Constant) and node.comparators[0].value == 3 ): self.errors.append(( node.left.lineno, node.left.col_offset, YTT201, )) elif ( self._is_sys('version', node.left) and len(node.ops) == 1 and isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and isinstance(node.comparators[0], ast.Constant) and isinstance(node.comparators[0].value, str) ): if len(node.comparators[0].value) == 1: code = YTT302 else: code = YTT103 self.errors.append(( node.left.lineno, node.left.col_offset, code, )) elif ( isinstance(node.left, ast.Subscript) and self._is_sys('version_info', node.left.value) and _is_index(node.left, n=1) and len(node.ops) == 1 and isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and isinstance(node.comparators[0], ast.Constant) and isinstance(node.comparators[0].value, (int, float)) ): self.errors.append(( node.lineno, node.col_offset, YTT203, )) elif ( isinstance(node.left, ast.Attribute) and self._is_sys('version_info', node.left.value) and node.left.attr == 'minor' and len(node.ops) == 1 and isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)) and isinstance(node.comparators[0], ast.Constant) and isinstance(node.comparators[0].value, (int, float)) ): self.errors.append(( node.lineno, node.col_offset, YTT204, )) self.generic_visit(node) def visit_Attribute(self, node: ast.Attribute) -> None: if ( isinstance(node.value, ast.Name) and node.value.id == 'six' and node.attr == 'PY3' ): self.errors.append((node.lineno, node.col_offset, YTT202)) self.generic_visit(node) def visit_Name(self, node: ast.Name) -> None: if node.id == 'PY3' and self._from_imports.get(node.id) == 'six': self.errors.append((node.lineno, node.col_offset, YTT202)) self.generic_visit(node) class Plugin: def __init__(self, tree: ast.AST): self._tree = tree def run(self) -> Generator[tuple[int, int, str, type[Any]], None, None]: visitor = Visitor() visitor.visit(self._tree) for line, col, msg in visitor.errors: yield line, col, msg, type(self) flake8-2020-1.8.1/requirements-dev.txt000066400000000000000000000000411444114451100173060ustar00rootroot00000000000000covdefaults>=2.1 coverage pytest flake8-2020-1.8.1/setup.cfg000066400000000000000000000020641444114451100150760ustar00rootroot00000000000000[metadata] name = flake8_2020 version = 1.8.1 description = flake8 plugin which checks for misuse of `sys.version` or `sys.version_info` long_description = file: README.md long_description_content_type = text/markdown url = https://github.com/asottile/flake8-2020 author = Anthony Sottile author_email = asottile@umich.edu license = MIT license_files = LICENSE classifiers = License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython [options] py_modules = flake8_2020 install_requires = flake8>=5 python_requires = >=3.8 [options.entry_points] flake8.extension = YTT=flake8_2020:Plugin [bdist_wheel] universal = True [coverage:run] plugins = covdefaults [mypy] check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_defs = true warn_redundant_casts = true warn_unused_ignores = true [mypy-testing.*] disallow_untyped_defs = false [mypy-tests.*] disallow_untyped_defs = false flake8-2020-1.8.1/setup.py000066400000000000000000000001111444114451100147560ustar00rootroot00000000000000from __future__ import annotations from setuptools import setup setup() flake8-2020-1.8.1/tests/000077500000000000000000000000001444114451100144155ustar00rootroot00000000000000flake8-2020-1.8.1/tests/__init__.py000066400000000000000000000000001444114451100165140ustar00rootroot00000000000000flake8-2020-1.8.1/tests/flake8_2020_test.py000066400000000000000000000105301444114451100176420ustar00rootroot00000000000000from __future__ import annotations import ast import pytest from flake8_2020 import Plugin def results(s): return {'{}:{}: {}'.format(*r) for r in Plugin(ast.parse(s)).run()} @pytest.mark.parametrize( 's', ( '', 'import sys\nprint(sys.version)', 'import sys\nprint("{}.{}".format(*sys.version_info))', 'PY3 = sys.version_info[0] >= 3', # ignore from imports with aliases, patches welcome 'from sys import version as v\nprint(v[:3])', # the tool is timid and only flags certain numeric slices 'import sys\nprint(sys.version[:i])', ), ) def test_ok(s): assert results(s) == set() @pytest.mark.parametrize( 's', ( 'import sys\nprint(sys.version[:3])', 'from sys import version\nprint(version[:3])', ), ) def test_py310_slicing_of_sys_version_string(s): assert results(s) == { '2:6: YTT101 `sys.version[:3]` referenced (python3.10), use ' '`sys.version_info`', } @pytest.mark.parametrize( 's', ( 'import sys\npy_minor = sys.version[2]', 'from sys import version\npy_minor = version[2]', ), ) def test_py310_indexing_of_sys_version_string(s): assert results(s) == { '2:11: YTT102 `sys.version[2]` referenced (python3.10), use ' '`sys.version_info`', } @pytest.mark.parametrize( 's', ( 'from sys import version\nversion < "3.5"', 'import sys\nsys.version < "3.5"', 'import sys\nsys.version <= "3.5"', 'import sys\nsys.version > "3.5"', 'import sys\nsys.version >= "3.5"', ), ) def test_py310_string_comparison(s): assert results(s) == { '2:0: YTT103 `sys.version` compared to string (python3.10), use ' '`sys.version_info`', } @pytest.mark.parametrize( 's', ( 'from sys import version\nversion < "3"', 'import sys\nsys.version < "3"', 'import sys\nsys.version <= "3"', 'import sys\nsys.version > "3"', 'import sys\nsys.version >= "3"', ), ) def test_py310_string_comparison_of_1_char(s): assert results(s) == { '2:0: YTT302 `sys.version` compared to string (python10), use ' '`sys.version_info`', } @pytest.mark.parametrize( 's', ( 'import sys\nPY3 = sys.version_info[0] == 3', 'from sys import version_info\nPY3 = version_info[0] == 3', 'import sys\nPY2 = sys.version_info[0] != 3', 'from sys import version_info\nPY2 = version_info[0] != 3', ), ) def test_py4_comparison_to_version_3(s): assert results(s) == { '2:6: YTT201 `sys.version_info[0] == 3` referenced (python4), use ' '`>=`', } @pytest.mark.parametrize( 's', ( 'import six\n' 'if six.PY3:\n' ' print("3")\n', 'from six import PY3\n' 'if PY3:\n' ' print("3")\n', ), ) def test_py4_usage_of_six_py3(s): assert results(s) == { '2:3: YTT202 `six.PY3` referenced (python4), use `not six.PY2`', } @pytest.mark.parametrize( 's', ( 'import sys\npy_major = sys.version[0]', 'from sys import version\npy_major = version[0]', ), ) def test_py10_indexing_of_sys_version_string(s): assert results(s) == { '2:11: YTT301 `sys.version[0]` referenced (python10), use ' '`sys.version_info`', } @pytest.mark.parametrize( 's', ( 'import sys\nprint(sys.version[:1])', 'from sys import version\nprint(version[:1])', ), ) def test_py10_slicing_of_sys_version_string(s): assert results(s) == { '2:6: YTT303 `sys.version[:1]` referenced (python10), use ' '`sys.version_info`', } @pytest.mark.parametrize( 's', ( 'import sys\nsys.version_info[1] >= 5', 'from sys import version_info\nversion_info[1] < 6', ), ) def test_version_info_index_one(s): assert results(s) == { '2:0: YTT203 `sys.version_info[1]` compared to integer (python4), ' 'compare `sys.version_info` to tuple', } @pytest.mark.parametrize( 's', ( 'import sys\nsys.version_info.minor <= 7', 'from sys import version_info\nversion_info.minor > 8', ), ) def test_version_info_minor(s): assert results(s) == { '2:0: YTT204 `sys.version_info.minor` compared to integer (python4), ' 'compare `sys.version_info` to tuple', } flake8-2020-1.8.1/tox.ini000066400000000000000000000004751444114451100145740ustar00rootroot00000000000000[tox] envlist = py,pre-commit [testenv] deps = -rrequirements-dev.txt commands = coverage erase coverage run -m pytest {posargs:tests} coverage report [testenv:pre-commit] skip_install = true deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure [pep8] ignore = E265,E501,W504