diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..d74ca19 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,26 @@ +[run] +branch = True +source = + flake8 + tests +omit = + # Don't complain if non-runnable code isn't run + */__main__.py + +[report] +show_missing = True +skip_covered = True +exclude_lines = + # Have to re-enable the standard pragma + \#\s*pragma: no cover + + # Don't complain if tests don't hit defensive assertion code: + ^\s*raise AssertionError\b + ^\s*raise NotImplementedError\b + ^\s*return NotImplemented\b + ^\s*raise$ + + # Don't complain if non-runnable code isn't run: + ^if __name__ == ['"]__main__['"]:$ + ^\s*if False: + ^\s*if TYPE_CHECKING: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 486b0cb..e210204 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,7 +13,13 @@ jobs: include: # linux - os: ubuntu-latest - python: pypy-3.11 + python: pypy-3.9 + toxenv: py + - os: ubuntu-latest + python: 3.8 + toxenv: py + - os: ubuntu-latest + python: 3.9 toxenv: py - os: ubuntu-latest python: '3.10' @@ -22,17 +28,11 @@ jobs: python: '3.11' toxenv: py - os: ubuntu-latest - python: '3.12' - toxenv: py - - os: ubuntu-latest - python: '3.13' - toxenv: py - - os: ubuntu-latest - python: '3.14' + python: '3.12-dev' toxenv: py # windows - os: windows-latest - python: '3.10' + python: 3.8 toxenv: py # misc - os: ubuntu-latest @@ -46,8 +46,8 @@ jobs: toxenv: dogfood runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - run: python -mpip install --upgrade setuptools pip tox virtualenv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f75e5ee..6de9c1b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,6 @@ repos: -- repo: https://github.com/asottile/add-trailing-comma - rev: v4.0.0 - hooks: - - id: add-trailing-comma - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 + rev: v4.4.0 hooks: - id: check-yaml - id: debug-statements @@ -12,33 +8,34 @@ repos: - id: trailing-whitespace exclude: ^tests/fixtures/ - repo: https://github.com/asottile/setup-cfg-fmt - rev: v3.2.0 + rev: v2.4.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.16.0 + rev: v3.10.0 hooks: - id: reorder-python-imports args: [ --application-directories, '.:src', - --py310-plus, + --py38-plus, --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.21.2 + rev: v3.9.0 hooks: - id: pyupgrade - args: [--py310-plus] -- repo: https://github.com/hhatto/autopep8 - rev: v2.3.2 + args: [--py38-plus] +- repo: https://github.com/psf/black + rev: 23.7.0 hooks: - - id: autopep8 + - id: black + args: [--line-length=79] - repo: https://github.com/PyCQA/flake8 - rev: 7.3.0 + rev: 6.0.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.1 + rev: v1.4.1 hooks: - id: mypy exclude: ^(docs/|example-plugin/) diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index dfa8b9d..0000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2 - -build: - os: ubuntu-22.04 - tools: - python: "3.11" -python: - install: - - path: . - - requirements: docs/source/requirements.txt -sphinx: - configuration: docs/source/conf.py diff --git a/bin/gen-pycodestyle-plugin b/bin/gen-pycodestyle-plugin index 7fc504a..8bc2efc 100755 --- a/bin/gen-pycodestyle-plugin +++ b/bin/gen-pycodestyle-plugin @@ -3,9 +3,9 @@ from __future__ import annotations import inspect import os.path -from collections.abc import Callable -from collections.abc import Generator from typing import Any +from typing import Callable +from typing import Generator from typing import NamedTuple import pycodestyle @@ -42,7 +42,7 @@ class Call(NamedTuple): return cls(func.__name__, inspect.isgeneratorfunction(func), params) -def lines() -> Generator[str]: +def lines() -> Generator[str, None, None]: logical = [] physical = [] @@ -58,8 +58,8 @@ def lines() -> Generator[str]: yield "# fmt: off" yield "from __future__ import annotations" yield "" - yield "from collections.abc import Generator" yield "from typing import Any" + yield "from typing import Generator" yield "" imports = sorted(call.name for call in logical + physical) for name in imports: @@ -71,7 +71,7 @@ def lines() -> Generator[str]: logical_params = {param for call in logical for param in call.params} for param in sorted(logical_params): yield f" {param}: Any," - yield ") -> Generator[tuple[int, str]]:" + yield ") -> Generator[tuple[int, str], None, None]:" yield ' """Run pycodestyle logical checks."""' for call in sorted(logical): yield call.to_src() @@ -82,7 +82,7 @@ def lines() -> Generator[str]: physical_params = {param for call in physical for param in call.params} for param in sorted(physical_params): yield f" {param}: Any," - yield ") -> Generator[tuple[int, str]]:" + yield ") -> Generator[tuple[int, str], None, None]:" yield ' """Run pycodestyle physical checks."""' for call in sorted(physical): yield call.to_src() diff --git a/docs/source/conf.py b/docs/source/conf.py index 48f8a52..a2b4af3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -34,7 +34,7 @@ extensions = [ "sphinx.ext.todo", "sphinx.ext.coverage", "sphinx.ext.viewcode", - "sphinx_prompt", + "sphinx-prompt", ] # Add any paths that contain templates here, relative to this directory. @@ -296,11 +296,7 @@ texinfo_documents = [ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = { - "python": ("https://docs.python.org/3/", None), - "packaging": ("https://packaging.python.org/en/latest/", None), - "setuptools": ("https://setuptools.pypa.io/en/latest/", None), -} +intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} extlinks = { "issue": ("https://github.com/pycqa/flake8/issues/%s", "#%s"), diff --git a/docs/source/internal/releases.rst b/docs/source/internal/releases.rst index d71796d..fc29bd3 100644 --- a/docs/source/internal/releases.rst +++ b/docs/source/internal/releases.rst @@ -30,7 +30,7 @@ Historically, |Flake8| has generated major releases for: - Large scale refactoring (2.0, 3.0, 5.0, 6.0) -- Subtly breaking CLI changes (3.0, 4.0, 5.0, 6.0, 7.0) +- Subtly breaking CLI changes (3.0, 4.0, 5.0, 6.0) - Breaking changes to its plugin interface (3.0) @@ -81,9 +81,9 @@ for users. Before releasing, the following tox test environments must pass: -- Python 3.9 (a.k.a., ``tox -e py39``) +- Python 3.8 (a.k.a., ``tox -e py38``) -- Python 3.13 (a.k.a., ``tox -e py313``) +- Python 3.12 (a.k.a., ``tox -e py312``) - PyPy 3 (a.k.a., ``tox -e pypy3``) diff --git a/docs/source/plugin-development/index.rst b/docs/source/plugin-development/index.rst index 9088942..c89e5f0 100644 --- a/docs/source/plugin-development/index.rst +++ b/docs/source/plugin-development/index.rst @@ -30,8 +30,7 @@ To get started writing a |Flake8| :term:`plugin` you first need: Once you've gathered these things, you can get started. -All plugins for |Flake8| must be registered via -:external+packaging:doc:`entry points`. In this +All plugins for |Flake8| must be registered via `entry points`_. In this section we cover: - How to register your plugin so |Flake8| can find it @@ -55,8 +54,6 @@ Here's a tutorial which goes over building an ast checking plugin from scratch: -Detailed Plugin Development Documentation -========================================= .. toctree:: :caption: Plugin Developer Documentation @@ -65,3 +62,7 @@ Detailed Plugin Development Documentation registering-plugins plugin-parameters formatters + + +.. _entry points: + https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points diff --git a/docs/source/plugin-development/registering-plugins.rst b/docs/source/plugin-development/registering-plugins.rst index 964ff99..ca74008 100644 --- a/docs/source/plugin-development/registering-plugins.rst +++ b/docs/source/plugin-development/registering-plugins.rst @@ -12,17 +12,16 @@ To register any kind of plugin with |Flake8|, you need: #. A name for your plugin that will (ideally) be unique. -|Flake8| relies on functionality provided by build tools called -:external+packaging:doc:`entry points`. These -allow any package to register a plugin with |Flake8| via that package's -metadata. +#. A somewhat recent version of setuptools (newer than 0.7.0 but preferably as + recent as you can attain). + +|Flake8| relies on functionality provided by setuptools called +`Entry Points`_. These allow any package to register a plugin with |Flake8| +via that package's ``setup.py`` file. Let's presume that we already have our plugin written and it's in a module -called ``flake8_example``. We will also assume ``setuptools`` is used as a -:external+packaging:term:`Build Backend`, but be aware that most backends -support entry points. - -We might have a ``setup.py`` that looks something like: +called ``flake8_example``. We might have a ``setup.py`` that looks something +like: .. code-block:: python @@ -151,7 +150,5 @@ If your plugin is intended to be opt-in, it can set the attribute :ref:`enable-extensions` with your plugin's entry point. -.. seealso:: - - The :external+setuptools:doc:`setuptools user guide ` - about entry points. +.. _Entry Points: + https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points diff --git a/docs/source/release-notes/7.0.0.rst b/docs/source/release-notes/7.0.0.rst deleted file mode 100644 index 6cd852a..0000000 --- a/docs/source/release-notes/7.0.0.rst +++ /dev/null @@ -1,19 +0,0 @@ -7.0.0 -- 2024-01-04 -------------------- - -You can view the `7.0.0 milestone`_ on GitHub for more details. - -Backwards Incompatible Changes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Remove ``--include-in-doctest`` and ``--exclude-from-doctest`` options. - (See also :issue:`1747`, :pull:`1854`) - -New Dependency Information -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Pyflakes has been updated to >= 3.2.0, < 3.3.0 (See also :pull:`1906`). - -.. all links -.. _7.0.0 milestone: - https://github.com/PyCQA/flake8/milestone/49 diff --git a/docs/source/release-notes/7.1.0.rst b/docs/source/release-notes/7.1.0.rst deleted file mode 100644 index 2229baa..0000000 --- a/docs/source/release-notes/7.1.0.rst +++ /dev/null @@ -1,13 +0,0 @@ -7.1.0 -- 2024-06-15 -------------------- - -You can view the `7.1.0 milestone`_ on GitHub for more details. - -New Dependency Information -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- pycodestyle has been updated to >= 2.12.0, < 2.13.0 (See also :pull:`1939`). - -.. all links -.. _7.1.0 milestone: - https://github.com/PyCQA/flake8/milestone/50 diff --git a/docs/source/release-notes/7.1.1.rst b/docs/source/release-notes/7.1.1.rst deleted file mode 100644 index 62f2d11..0000000 --- a/docs/source/release-notes/7.1.1.rst +++ /dev/null @@ -1,15 +0,0 @@ -7.1.1 -- 2024-08-04 -------------------- - -You can view the `7.1.1 milestone`_ on GitHub for more details. - -Bugs Fixed -~~~~~~~~~~ - -- Properly preserve escaped `{` and `}` in fstrings in logical lines in 3.12+. - (See also :issue:`1948`, :pull:`1949`). - - -.. all links -.. _7.1.1 milestone: - https://github.com/PyCQA/flake8/milestone/51 diff --git a/docs/source/release-notes/7.1.2.rst b/docs/source/release-notes/7.1.2.rst deleted file mode 100644 index 010656c..0000000 --- a/docs/source/release-notes/7.1.2.rst +++ /dev/null @@ -1,15 +0,0 @@ -7.1.2 -- 2025-02-16 -------------------- - -You can view the `7.1.2 milestone`_ on GitHub for more details. - -Bugs Fixed -~~~~~~~~~~ - -- Avoid starting unnecessary processes when "# files" < "jobs". - (See also :pull:`1966`). - - -.. all links -.. _7.1.2 milestone: - https://github.com/PyCQA/flake8/milestone/52 diff --git a/docs/source/release-notes/7.2.0.rst b/docs/source/release-notes/7.2.0.rst deleted file mode 100644 index fe124d7..0000000 --- a/docs/source/release-notes/7.2.0.rst +++ /dev/null @@ -1,19 +0,0 @@ -7.2.0 -- 2025-03-29 -------------------- - -You can view the `7.2.0 milestone`_ on GitHub for more details. - -New Dependency Information -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- pycodestyle has been updated to >= 2.13.0, < 2.14.0 (See also :pull:`1974`). -- pyflakes has been updated to >= 3.3.0, < 3.4.0 (See also :pull:`1974`). - -Features -~~~~~~~~ - -- Require python >= 3.9 (See also :pull:`1973`). - -.. all links -.. _7.2.0 milestone: - https://github.com/PyCQA/flake8/milestone/53 diff --git a/docs/source/release-notes/7.3.0.rst b/docs/source/release-notes/7.3.0.rst deleted file mode 100644 index dedc918..0000000 --- a/docs/source/release-notes/7.3.0.rst +++ /dev/null @@ -1,15 +0,0 @@ -7.3.0 -- 2025-06-20 -------------------- - -You can view the `7.3.0 milestone`_ on GitHub for more details. - -New Dependency Information -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Added support for python 3.14 (See also :pull:`1983`). -- pycodestyle has been updated to >= 2.14.0, < 2.15.0 (See also :pull:`1985`). -- Pyflakes has been updated to >= 3.4.0, < 3.5.0 (See also :pull:`1985`). - -.. all links -.. _7.3.0 milestone: - https://github.com/PyCQA/flake8/milestone/54 diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index 10697df..8cd7573 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -5,23 +5,12 @@ All of the release notes that have been recorded for Flake8 are organized here with the newest releases first. -7.x Release Series -================== - -.. toctree:: - 7.3.0 - 7.2.0 - 7.1.2 - 7.1.1 - 7.1.0 - 7.0.0 - 6.x Release Series ================== .. toctree:: - 6.1.0 6.0.0 + 6.1.0 5.x Release Series ================== diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt index 765fb13..da3b991 100644 --- a/docs/source/requirements.txt +++ b/docs/source/requirements.txt @@ -1,4 +1,4 @@ sphinx>=2.1.0,!=3.1.0 -sphinx-rtd-theme>=1.2.2 -sphinx-prompt>=1.8.0 +sphinx_rtd_theme +sphinx-prompt docutils!=0.18 diff --git a/docs/source/user/error-codes.rst b/docs/source/user/error-codes.rst index c8b46c1..2a91413 100644 --- a/docs/source/user/error-codes.rst +++ b/docs/source/user/error-codes.rst @@ -59,8 +59,6 @@ generates its own :term:`error code`\ s for ``pyflakes``: +------+---------------------------------------------------------------------+ | F541 | f-string without any placeholders | +------+---------------------------------------------------------------------+ -| F542 | t-string without any placeholders | -+------+---------------------------------------------------------------------+ +------+---------------------------------------------------------------------+ | F601 | dictionary key ``name`` repeated with different values | +------+---------------------------------------------------------------------+ @@ -104,9 +102,6 @@ generates its own :term:`error code`\ s for ``pyflakes``: +------+---------------------------------------------------------------------+ | F823 | local variable ``name`` ... referenced before assignment | +------+---------------------------------------------------------------------+ -| F824 | ``global name`` / ``nonlocal name`` is unused: name is never | -| | assigned in scope | -+------+---------------------------------------------------------------------+ | F831 | duplicate argument ``name`` in function definition | +------+---------------------------------------------------------------------+ | F841 | local variable ``name`` is assigned to but never used | diff --git a/docs/source/user/invocation.rst b/docs/source/user/invocation.rst index 10895dd..0049ec9 100644 --- a/docs/source/user/invocation.rst +++ b/docs/source/user/invocation.rst @@ -14,25 +14,25 @@ like so: Where you simply allow the shell running in your terminal to locate |Flake8|. In some cases, though, you may have installed |Flake8| for multiple versions -of Python (e.g., Python 3.13 and Python 3.14) and you need to call a specific +of Python (e.g., Python 3.8 and Python 3.9) and you need to call a specific version. In that case, you will have much better results using: .. prompt:: bash - python3.13 -m flake8 + python3.8 -m flake8 Or .. prompt:: bash - python3.14 -m flake8 + python3.9 -m flake8 Since that will tell the correct version of Python to run |Flake8|. .. note:: - Installing |Flake8| once will not install it on both Python 3.13 and - Python 3.14. It will only install it for the version of Python that + Installing |Flake8| once will not install it on both Python 3.8 and + Python 3.9. It will only install it for the version of Python that is running pip. It is also possible to specify command-line options directly to |Flake8|: @@ -51,7 +51,7 @@ Or This is the last time we will show both versions of an invocation. From now on, we'll simply use ``flake8`` and assume that the user - knows they can instead use ``python -m flake8``. + knows they can instead use ``python -m flake8`` instead. It's also possible to narrow what |Flake8| will try to check by specifying exactly the paths and directories you want it to check. Let's assume that diff --git a/docs/source/user/options.rst b/docs/source/user/options.rst index bd80c87..8db2df8 100644 --- a/docs/source/user/options.rst +++ b/docs/source/user/options.rst @@ -46,8 +46,6 @@ Index of Options - :option:`flake8 --exclude` -- :option:`flake8 --extend-exclude` - - :option:`flake8 --filename` - :option:`flake8 --stdin-display-name` @@ -100,6 +98,10 @@ Index of Options - :option:`flake8 --doctests` +- :option:`flake8 --include-in-doctest` + +- :option:`flake8 --exclude-from-doctest` + - :option:`flake8 --benchmark` - :option:`flake8 --bug-report` @@ -995,6 +997,62 @@ Options and their Descriptions doctests = True +.. option:: --include-in-doctest= + + :ref:`Go back to index ` + + Specify which files are checked by PyFlakes for doctest syntax. + + This is registered by the default PyFlakes plugin. + + Command-line example: + + .. prompt:: bash + + flake8 --include-in-doctest=dir/subdir/file.py,dir/other/file.py dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + include-in-doctest = + dir/subdir/file.py, + dir/other/file.py + include_in_doctest = + dir/subdir/file.py, + dir/other/file.py + + +.. option:: --exclude-from-doctest= + + :ref:`Go back to index ` + + Specify which files are not to be checked by PyFlakes for doctest syntax. + + This is registered by the default PyFlakes plugin. + + Command-line example: + + .. prompt:: bash + + flake8 --exclude-from-doctest=dir/subdir/file.py,dir/other/file.py dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + exclude-from-doctest = + dir/subdir/file.py, + dir/other/file.py + exclude_from_doctest = + dir/subdir/file.py, + dir/other/file.py + + .. option:: --benchmark :ref:`Go back to index ` diff --git a/example-plugin/setup.py b/example-plugin/setup.py index 9e7c89f..c0720bd 100644 --- a/example-plugin/setup.py +++ b/example-plugin/setup.py @@ -23,6 +23,8 @@ setuptools.setup( "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Quality Assurance", ], diff --git a/setup.cfg b/setup.cfg index c0b8137..ebf4355 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,7 @@ classifiers = Environment :: Console Framework :: Flake8 Intended Audience :: Developers + License :: OSI Approved :: MIT License Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only @@ -28,9 +29,9 @@ classifiers = packages = find: install_requires = mccabe>=0.7.0,<0.8.0 - pycodestyle>=2.14.0,<2.15.0 - pyflakes>=3.4.0,<3.5.0 -python_requires = >=3.10 + pycodestyle>=2.11.0,<2.12.0 + pyflakes>=3.1.0,<3.2.0 +python_requires = >=3.8.1 package_dir = =src @@ -53,15 +54,6 @@ flake8.report = [bdist_wheel] universal = 1 -[coverage:run] -source = - flake8 - tests -plugins = covdefaults - -[coverage:report] -fail_under = 97 - [mypy] check_untyped_defs = true disallow_any_generics = true diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index 0dea638..171b1db 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ import sys LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "7.3.0" +__version__ = "6.1.0" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { @@ -66,5 +66,5 @@ def configure_logging( LOG.addHandler(handler) LOG.setLevel(log_level) LOG.debug( - "Added a %s logging handler to logger root at %s", filename, __name__, + "Added a %s logging handler to logger root at %s", filename, __name__ ) diff --git a/src/flake8/_compat.py b/src/flake8/_compat.py index 22bb84e..f4a0903 100644 --- a/src/flake8/_compat.py +++ b/src/flake8/_compat.py @@ -3,16 +3,9 @@ from __future__ import annotations import sys import tokenize -if sys.version_info >= (3, 12): # pragma: >=3.12 cover +if sys.version_info >= (3, 12): FSTRING_START = tokenize.FSTRING_START FSTRING_MIDDLE = tokenize.FSTRING_MIDDLE FSTRING_END = tokenize.FSTRING_END -else: # pragma: <3.12 cover +else: FSTRING_START = FSTRING_MIDDLE = FSTRING_END = -1 - -if sys.version_info >= (3, 14): # pragma: >=3.14 cover - TSTRING_START = tokenize.TSTRING_START - TSTRING_MIDDLE = tokenize.TSTRING_MIDDLE - TSTRING_END = tokenize.TSTRING_END -else: # pragma: <3.14 cover - TSTRING_START = TSTRING_MIDDLE = TSTRING_END = -1 diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py index 4d5c91d..446df29 100644 --- a/src/flake8/api/legacy.py +++ b/src/flake8/api/legacy.py @@ -135,7 +135,7 @@ class StyleGuide: stdin_display_name=self.options.stdin_display_name, filename_patterns=self.options.filename, exclude=self.options.exclude, - ), + ) ) return not paths @@ -153,7 +153,7 @@ class StyleGuide: if not issubclass(reporter, formatter.BaseFormatter): raise ValueError( "Report should be subclass of " - "flake8.formatter.BaseFormatter.", + "flake8.formatter.BaseFormatter." ) self._application.formatter = reporter(self.options) self._application.guide = None diff --git a/src/flake8/checker.py b/src/flake8/checker.py index c6a24eb..6c4caef 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -9,24 +9,25 @@ import multiprocessing.pool import operator import signal import tokenize -from collections.abc import Generator -from collections.abc import Sequence from typing import Any +from typing import Generator +from typing import List from typing import Optional +from typing import Sequence +from typing import Tuple from flake8 import defaults from flake8 import exceptions from flake8 import processor from flake8 import utils from flake8._compat import FSTRING_START -from flake8._compat import TSTRING_START from flake8.discover_files import expand_paths from flake8.options.parse_args import parse_args from flake8.plugins.finder import Checkers from flake8.plugins.finder import LoadedPlugin from flake8.style_guide import StyleGuideManager -Results = list[tuple[str, int, int, str, Optional[str]]] +Results = List[Tuple[str, int, int, str, Optional[str]]] LOG = logging.getLogger(__name__) @@ -45,39 +46,39 @@ SERIAL_RETRY_ERRNOS = { # noise in diffs. } -_mp: tuple[Checkers, argparse.Namespace] | None = None +_mp_plugins: Checkers +_mp_options: argparse.Namespace @contextlib.contextmanager def _mp_prefork( - plugins: Checkers, options: argparse.Namespace, -) -> Generator[None]: + plugins: Checkers, options: argparse.Namespace +) -> Generator[None, None, None]: # we can save significant startup work w/ `fork` multiprocessing - global _mp - _mp = plugins, options + global _mp_plugins, _mp_options + _mp_plugins, _mp_options = plugins, options try: yield finally: - _mp = None + del _mp_plugins, _mp_options def _mp_init(argv: Sequence[str]) -> None: - global _mp + global _mp_plugins, _mp_options # Ensure correct signaling of ^C using multiprocessing.Pool. signal.signal(signal.SIGINT, signal.SIG_IGN) - # for `fork` this'll already be set - if _mp is None: + try: + _mp_plugins, _mp_options # for `fork` this'll already be set + except NameError: plugins, options = parse_args(argv) - _mp = plugins.checkers, options + _mp_plugins, _mp_options = plugins.checkers, options def _mp_run(filename: str) -> tuple[str, Results, dict[str, int]]: - assert _mp is not None, _mp - plugins, options = _mp return FileChecker( - filename=filename, plugins=plugins, options=options, + filename=filename, plugins=_mp_plugins, options=_mp_options ).run_checks() @@ -137,7 +138,7 @@ class Manager: if utils.is_using_stdin(self.options.filenames): LOG.warning( "The --jobs option is not compatible with supplying " - "input using - . Ignoring --jobs arguments.", + "input using - . Ignoring --jobs arguments." ) return 0 @@ -252,9 +253,8 @@ class Manager: stdin_display_name=self.options.stdin_display_name, filename_patterns=self.options.filename, exclude=self.exclude, - ), + ) ) - self.jobs = min(len(self.filenames), self.jobs) def stop(self) -> None: """Stop checking files.""" @@ -332,11 +332,11 @@ class FileChecker: assert self.processor is not None, self.filename try: params = self.processor.keyword_arguments_for( - plugin.parameters, arguments, + plugin.parameters, arguments ) except AttributeError as ae: raise exceptions.PluginRequestedUnknownParameters( - plugin_name=plugin.display_name, exception=ae, + plugin_name=plugin.display_name, exception=ae ) try: return plugin.obj(**arguments, **params) @@ -372,6 +372,43 @@ class FileChecker: token = () row, column = (1, 0) + if ( + column > 0 + and token + and isinstance(exception, SyntaxError) + and len(token) == 4 # Python 3.9 or earlier + ): + # NOTE(sigmavirus24): SyntaxErrors report 1-indexed column + # numbers. We need to decrement the column number by 1 at + # least. + column_offset = 1 + row_offset = 0 + # See also: https://github.com/pycqa/flake8/issues/169, + # https://github.com/PyCQA/flake8/issues/1372 + # On Python 3.9 and earlier, token will be a 4-item tuple with the + # last item being the string. Starting with 3.10, they added to + # the tuple so now instead of it ending with the code that failed + # to parse, it ends with the end of the section of code that + # failed to parse. Luckily the absolute position in the tuple is + # stable across versions so we can use that here + physical_line = token[3] + + # NOTE(sigmavirus24): Not all "tokens" have a string as the last + # argument. In this event, let's skip trying to find the correct + # column and row values. + if physical_line is not None: + # NOTE(sigmavirus24): SyntaxErrors also don't exactly have a + # "physical" line so much as what was accumulated by the point + # tokenizing failed. + # See also: https://github.com/pycqa/flake8/issues/169 + lines = physical_line.rstrip("\n").split("\n") + row_offset = len(lines) - 1 + logical_line = lines[0] + logical_line_length = len(logical_line) + if column > logical_line_length: + column = logical_line_length + row -= row_offset + column -= column_offset return row, column def run_ast_checks(self) -> None: @@ -511,14 +548,12 @@ class FileChecker: self.run_logical_checks() def check_physical_eol( - self, token: tokenize.TokenInfo, prev_physical: str, + self, token: tokenize.TokenInfo, prev_physical: str ) -> None: """Run physical checks if and only if it is at the end of the line.""" assert self.processor is not None if token.type == FSTRING_START: # pragma: >=3.12 cover self.processor.fstring_start(token.start[0]) - elif token.type == TSTRING_START: # pragma: >=3.14 cover - self.processor.tstring_start(token.start[0]) # a newline token ends a single physical line. elif processor.is_eol_token(token): # if the file does not end with a newline, the NEWLINE @@ -561,7 +596,7 @@ def _try_initialize_processpool( def find_offset( - offset: int, mapping: processor._LogicalMapping, + offset: int, mapping: processor._LogicalMapping ) -> tuple[int, int]: """Find the offset tuple for a single offset.""" if isinstance(offset, tuple): diff --git a/src/flake8/discover_files.py b/src/flake8/discover_files.py index 40b6e5c..580d5fd 100644 --- a/src/flake8/discover_files.py +++ b/src/flake8/discover_files.py @@ -3,9 +3,9 @@ from __future__ import annotations import logging import os.path -from collections.abc import Callable -from collections.abc import Generator -from collections.abc import Sequence +from typing import Callable +from typing import Generator +from typing import Sequence from flake8 import utils @@ -16,7 +16,7 @@ def _filenames_from( arg: str, *, predicate: Callable[[str], bool], -) -> Generator[str]: +) -> Generator[str, None, None]: """Generate filenames from an argument. :param arg: @@ -55,7 +55,7 @@ def expand_paths( stdin_display_name: str, filename_patterns: Sequence[str], exclude: Sequence[str], -) -> Generator[str]: +) -> Generator[str, None, None]: """Expand out ``paths`` from commandline to the lintable files.""" if not paths: paths = ["."] diff --git a/src/flake8/formatting/base.py b/src/flake8/formatting/base.py index bbbfdff..d986d65 100644 --- a/src/flake8/formatting/base.py +++ b/src/flake8/formatting/base.py @@ -110,7 +110,7 @@ class BaseFormatter: The formatted error string. """ raise NotImplementedError( - "Subclass of BaseFormatter did not implement" " format.", + "Subclass of BaseFormatter did not implement" " format." ) def show_statistics(self, statistics: Statistics) -> None: diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 165a6ef..b6bfae3 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -5,7 +5,7 @@ import argparse import json import logging import time -from collections.abc import Sequence +from typing import Sequence import flake8 from flake8 import checker @@ -76,7 +76,7 @@ class Application: assert self.formatter is not None assert self.options is not None self.guide = style_guide.StyleGuideManager( - self.options, self.formatter, + self.options, self.formatter ) def make_file_checker_manager(self, argv: Sequence[str]) -> None: diff --git a/src/flake8/main/cli.py b/src/flake8/main/cli.py index 1a52f36..01a67ac 100644 --- a/src/flake8/main/cli.py +++ b/src/flake8/main/cli.py @@ -2,7 +2,7 @@ from __future__ import annotations import sys -from collections.abc import Sequence +from typing import Sequence from flake8.main import application diff --git a/src/flake8/main/debug.py b/src/flake8/main/debug.py index 73ca74b..c3a8b0b 100644 --- a/src/flake8/main/debug.py +++ b/src/flake8/main/debug.py @@ -14,7 +14,7 @@ def information(version: str, plugins: Plugins) -> dict[str, Any]: (loaded.plugin.package, loaded.plugin.version) for loaded in plugins.all_plugins() if loaded.plugin.package not in {"flake8", "local"} - }, + } ) return { "version": version, diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index e8cbe09..9d57321 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -32,7 +32,7 @@ def stage1_arg_parser() -> argparse.ArgumentParser: ) parser.add_argument( - "--output-file", default=None, help="Redirect report to a file.", + "--output-file", default=None, help="Redirect report to a file." ) # Config file options diff --git a/src/flake8/options/aggregator.py b/src/flake8/options/aggregator.py index 999161a..af8e744 100644 --- a/src/flake8/options/aggregator.py +++ b/src/flake8/options/aggregator.py @@ -8,7 +8,7 @@ from __future__ import annotations import argparse import configparser import logging -from collections.abc import Sequence +from typing import Sequence from flake8.options import config from flake8.options.manager import OptionManager diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py index fddee55..b51949c 100644 --- a/src/flake8/options/config.py +++ b/src/flake8/options/config.py @@ -78,7 +78,7 @@ def load_config( if config is not None: if not cfg.read(config, encoding="UTF-8"): raise exceptions.ExecutionError( - f"The specified config file does not exist: {config}", + f"The specified config file does not exist: {config}" ) cfg_dir = os.path.dirname(config) else: @@ -89,7 +89,7 @@ def load_config( for filename in extra: if not cfg.read(filename, encoding="UTF-8"): raise exceptions.ExecutionError( - f"The specified config file does not exist: {filename}", + f"The specified config file does not exist: {filename}" ) return cfg, cfg_dir @@ -131,7 +131,7 @@ def parse_config( raise ValueError( f"Error code {error_code!r} " f"supplied to {option_name!r} option " - f"does not match {VALID_CODE_PREFIX.pattern!r}", + f"does not match {VALID_CODE_PREFIX.pattern!r}" ) assert option.config_name is not None diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index ae40794..4fd26b2 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -5,9 +5,9 @@ import argparse import enum import functools import logging -from collections.abc import Callable -from collections.abc import Sequence from typing import Any +from typing import Callable +from typing import Sequence from flake8 import utils from flake8.plugins.finder import Plugins @@ -165,7 +165,7 @@ class Option: if long_option_name is _ARG.NO: raise ValueError( "When specifying parse_from_config=True, " - "a long_option_name must also be specified.", + "a long_option_name must also be specified." ) self.config_name = long_option_name[2:].replace("-", "_") diff --git a/src/flake8/options/parse_args.py b/src/flake8/options/parse_args.py index ff5e08f..e3f8795 100644 --- a/src/flake8/options/parse_args.py +++ b/src/flake8/options/parse_args.py @@ -2,7 +2,7 @@ from __future__ import annotations import argparse -from collections.abc import Sequence +from typing import Sequence import flake8 from flake8.main import options diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py index 4da3402..380ec3a 100644 --- a/src/flake8/plugins/finder.py +++ b/src/flake8/plugins/finder.py @@ -7,9 +7,9 @@ import inspect import itertools import logging import sys -from collections.abc import Generator -from collections.abc import Iterable from typing import Any +from typing import Generator +from typing import Iterable from typing import NamedTuple from flake8 import utils @@ -68,7 +68,7 @@ class Plugins(NamedTuple): reporters: dict[str, LoadedPlugin] disabled: list[LoadedPlugin] - def all_plugins(self) -> Generator[LoadedPlugin]: + def all_plugins(self) -> Generator[LoadedPlugin, None, None]: """Return an iterator over all :class:`LoadedPlugin`s.""" yield from self.checkers.tree yield from self.checkers.logical_line @@ -83,8 +83,8 @@ class Plugins(NamedTuple): f"{loaded.plugin.package}: {loaded.plugin.version}" for loaded in self.all_plugins() if loaded.plugin.package not in {"flake8", "local"} - }, - ), + } + ) ) @@ -151,7 +151,7 @@ def _flake8_plugins( eps: Iterable[importlib.metadata.EntryPoint], name: str, version: str, -) -> Generator[Plugin]: +) -> Generator[Plugin, None, None]: pyflakes_meta = importlib.metadata.distribution("pyflakes").metadata pycodestyle_meta = importlib.metadata.distribution("pycodestyle").metadata @@ -167,13 +167,13 @@ def _flake8_plugins( # ideally pycodestyle's plugin entrypoints would exactly represent # the codes they produce... yield Plugin( - pycodestyle_meta["name"], pycodestyle_meta["version"], ep, + pycodestyle_meta["name"], pycodestyle_meta["version"], ep ) else: yield Plugin(name, version, ep) -def _find_importlib_plugins() -> Generator[Plugin]: +def _find_importlib_plugins() -> Generator[Plugin, None, None]: # some misconfigured pythons (RHEL) have things on `sys.path` twice seen = set() for dist in importlib.metadata.distributions(): @@ -212,7 +212,7 @@ def _find_importlib_plugins() -> Generator[Plugin]: def _find_local_plugins( cfg: configparser.RawConfigParser, -) -> Generator[Plugin]: +) -> Generator[Plugin, None, None]: for plugin_type in ("extension", "report"): group = f"flake8.{plugin_type}" for plugin_s in utils.parse_comma_separated_list( @@ -240,7 +240,7 @@ def _check_required_plugins( f"required plugins were not installed!\n" f"- installed: {', '.join(sorted(plugin_names))}\n" f"- expected: {', '.join(sorted(expected_names))}\n" - f"- missing: {', '.join(sorted(missing_plugins))}", + f"- missing: {', '.join(sorted(missing_plugins))}" ) @@ -338,7 +338,7 @@ def _classify_plugins( if not VALID_CODE_PREFIX.match(loaded.entry_name): raise ExecutionError( f"plugin code for `{loaded.display_name}` does not match " - f"{VALID_CODE_PREFIX.pattern}", + f"{VALID_CODE_PREFIX.pattern}" ) return Plugins( diff --git a/src/flake8/plugins/pycodestyle.py b/src/flake8/plugins/pycodestyle.py index cd760dc..9e1d2bb 100644 --- a/src/flake8/plugins/pycodestyle.py +++ b/src/flake8/plugins/pycodestyle.py @@ -2,8 +2,8 @@ # fmt: off from __future__ import annotations -from collections.abc import Generator from typing import Any +from typing import Generator from pycodestyle import ambiguous_identifier as _ambiguous_identifier from pycodestyle import bare_except as _bare_except @@ -55,7 +55,7 @@ def pycodestyle_logical( previous_unindented_logical_line: Any, tokens: Any, verbose: Any, -) -> Generator[tuple[int, str]]: +) -> Generator[tuple[int, str], None, None]: """Run pycodestyle logical checks.""" yield from _ambiguous_identifier(logical_line, tokens) yield from _bare_except(logical_line, noqa) @@ -93,7 +93,7 @@ def pycodestyle_physical( noqa: Any, physical_line: Any, total_lines: Any, -) -> Generator[tuple[int, str]]: +) -> Generator[tuple[int, str], None, None]: """Run pycodestyle physical checks.""" ret = _maximum_line_length(physical_line, max_line_length, multiline, line_number, noqa) # noqa: E501 if ret is not None: diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index 9844025..f62527e 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -4,11 +4,13 @@ from __future__ import annotations import argparse import ast import logging -from collections.abc import Generator +import os from typing import Any +from typing import Generator import pyflakes.checker +from flake8 import utils from flake8.options.manager import OptionManager LOG = logging.getLogger(__name__) @@ -36,7 +38,6 @@ FLAKE8_PYFLAKES_CODES = { "StringDotFormatMissingArgument": "F524", "StringDotFormatMixingAutomatic": "F525", "FStringMissingPlaceholders": "F541", - "TStringMissingPlaceholders": "F542", "MultiValueRepeatedKeyLiteral": "F601", "MultiValueRepeatedKeyVariable": "F602", "TooManyExpressionsInStarredAssignment": "F621", @@ -56,7 +57,6 @@ FLAKE8_PYFLAKES_CODES = { "UndefinedName": "F821", "UndefinedExport": "F822", "UndefinedLocal": "F823", - "UnusedIndirectAssignment": "F824", "DuplicateArgument": "F831", "UnusedVariable": "F841", "UnusedAnnotation": "F842", @@ -68,12 +68,34 @@ class FlakesChecker(pyflakes.checker.Checker): """Subclass the Pyflakes checker to conform with the flake8 API.""" with_doctest = False + include_in_doctest: list[str] = [] + exclude_from_doctest: list[str] = [] def __init__(self, tree: ast.AST, filename: str) -> None: """Initialize the PyFlakes plugin with an AST tree and filename.""" - super().__init__( - tree, filename=filename, withDoctest=self.with_doctest, - ) + filename = utils.normalize_path(filename) + with_doctest = self.with_doctest + included_by = [ + include + for include in self.include_in_doctest + if include != "" and filename.startswith(include) + ] + if included_by: + with_doctest = True + + for exclude in self.exclude_from_doctest: + if exclude != "" and filename.startswith(exclude): + with_doctest = False + overlapped_by = [ + include + for include in included_by + if include.startswith(exclude) + ] + + if overlapped_by: + with_doctest = True + + super().__init__(tree, filename=filename, withDoctest=with_doctest) @classmethod def add_options(cls, parser: OptionManager) -> None: @@ -91,6 +113,24 @@ class FlakesChecker(pyflakes.checker.Checker): parse_from_config=True, help="also check syntax of the doctests", ) + parser.add_option( + "--include-in-doctest", + default="", + dest="include_in_doctest", + parse_from_config=True, + comma_separated_list=True, + normalize_paths=True, + help="Run doctests only on these files", + ) + parser.add_option( + "--exclude-from-doctest", + default="", + dest="exclude_from_doctest", + parse_from_config=True, + comma_separated_list=True, + normalize_paths=True, + help="Skip these files when running doctests", + ) @classmethod def parse_options(cls, options: argparse.Namespace) -> None: @@ -99,7 +139,44 @@ class FlakesChecker(pyflakes.checker.Checker): cls.builtIns = cls.builtIns.union(options.builtins) cls.with_doctest = options.doctests - def run(self) -> Generator[tuple[int, int, str, type[Any]]]: + if options.include_in_doctest or options.exclude_from_doctest: + LOG.warning( + "--include-in-doctest / --exclude-from-doctest will be " + "removed in a future version. see PyCQA/flake8#1747" + ) + + included_files = [] + for included_file in options.include_in_doctest: + if included_file == "": + continue + if not included_file.startswith((os.sep, "./", "~/")): + included_files.append(f"./{included_file}") + else: + included_files.append(included_file) + cls.include_in_doctest = utils.normalize_paths(included_files) + + excluded_files = [] + for excluded_file in options.exclude_from_doctest: + if excluded_file == "": + continue + if not excluded_file.startswith((os.sep, "./", "~/")): + excluded_files.append(f"./{excluded_file}") + else: + excluded_files.append(excluded_file) + cls.exclude_from_doctest = utils.normalize_paths(excluded_files) + + inc_exc = set(cls.include_in_doctest).intersection( + cls.exclude_from_doctest + ) + if inc_exc: + raise ValueError( + f"{inc_exc!r} was specified in both the " + f"include-in-doctest and exclude-from-doctest " + f"options. You are not allowed to specify it in " + f"both for doctesting." + ) + + def run(self) -> Generator[tuple[int, int, str, type[Any]], None, None]: """Run the plugin.""" for message in self.messages: col = getattr(message, "col", 0) diff --git a/src/flake8/processor.py b/src/flake8/processor.py index b1742ca..2eea88f 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -3,29 +3,28 @@ from __future__ import annotations import argparse import ast -import functools import logging import tokenize -from collections.abc import Generator from typing import Any +from typing import Generator +from typing import List +from typing import Tuple from flake8 import defaults from flake8 import utils from flake8._compat import FSTRING_END from flake8._compat import FSTRING_MIDDLE -from flake8._compat import TSTRING_END -from flake8._compat import TSTRING_MIDDLE from flake8.plugins.finder import LoadedPlugin LOG = logging.getLogger(__name__) NEWLINE = frozenset([tokenize.NL, tokenize.NEWLINE]) SKIP_TOKENS = frozenset( - [tokenize.NL, tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT], + [tokenize.NL, tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT] ) -_LogicalMapping = list[tuple[int, tuple[int, int]]] -_Logical = tuple[list[str], list[str], _LogicalMapping] +_LogicalMapping = List[Tuple[int, Tuple[int, int]]] +_Logical = Tuple[List[str], List[str], _LogicalMapping] class FileProcessor: @@ -115,28 +114,32 @@ class FileProcessor: self.verbose = options.verbose #: Statistics dictionary self.statistics = {"logical lines": 0} - self._fstring_start = self._tstring_start = -1 + self._file_tokens: list[tokenize.TokenInfo] | None = None + # map from line number to the line we'll search for `noqa` in + self._noqa_line_mapping: dict[int, str] | None = None + self._fstring_start = -1 - @functools.cached_property + @property def file_tokens(self) -> list[tokenize.TokenInfo]: """Return the complete set of tokens for a file.""" - line_iter = iter(self.lines) - return list(tokenize.generate_tokens(lambda: next(line_iter))) + if self._file_tokens is None: + line_iter = iter(self.lines) + self._file_tokens = list( + tokenize.generate_tokens(lambda: next(line_iter)) + ) - def fstring_start(self, lineno: int) -> None: # pragma: >=3.12 cover + return self._file_tokens + + def fstring_start(self, lineno: int) -> None: """Signal the beginning of an fstring.""" self._fstring_start = lineno - def tstring_start(self, lineno: int) -> None: # pragma: >=3.14 cover - """Signal the beginning of an tstring.""" - self._tstring_start = lineno - - def multiline_string(self, token: tokenize.TokenInfo) -> Generator[str]: + def multiline_string( + self, token: tokenize.TokenInfo + ) -> Generator[str, None, None]: """Iterate through the lines of a multiline string.""" - if token.type == FSTRING_END: # pragma: >=3.12 cover + if token.type == FSTRING_END: start = self._fstring_start - elif token.type == TSTRING_END: # pragma: >=3.14 cover - start = self._tstring_start else: start = token.start[0] @@ -173,7 +176,7 @@ class FileProcessor: """Update the checker_state attribute for the plugin.""" if "checker_state" in plugin.parameters: self.checker_state = self._checker_states.setdefault( - plugin.entry_name, {}, + plugin.entry_name, {} ) def next_logical_line(self) -> None: @@ -206,18 +209,9 @@ class FileProcessor: continue if token_type == tokenize.STRING: text = mutate_string(text) - elif token_type in { - FSTRING_MIDDLE, - TSTRING_MIDDLE, - }: # pragma: >=3.12 cover # noqa: E501 - # A curly brace in an FSTRING_MIDDLE token must be an escaped - # curly brace. Both 'text' and 'end' will account for the - # escaped version of the token (i.e. a single brace) rather - # than the raw double brace version, so we must counteract this - brace_offset = text.count("{") + text.count("}") - text = "x" * (len(text) + brace_offset) - end = (end[0], end[1] + brace_offset) - if previous_row is not None and previous_column is not None: + elif token_type == FSTRING_MIDDLE: + text = "x" * len(text) + if previous_row: (start_row, start_column) = start if previous_row != start_row: row_index = previous_row - 1 @@ -270,7 +264,7 @@ class FileProcessor: ) return ret - def generate_tokens(self) -> Generator[tokenize.TokenInfo]: + def generate_tokens(self) -> Generator[tokenize.TokenInfo, None, None]: """Tokenize the file and yield the tokens.""" for token in tokenize.generate_tokens(self.next_line): if token[2][0] > self.total_lines: @@ -280,40 +274,44 @@ class FileProcessor: def _noqa_line_range(self, min_line: int, max_line: int) -> dict[int, str]: line_range = range(min_line, max_line + 1) - joined = "".join(self.lines[min_line - 1: max_line]) + joined = "".join(self.lines[min_line - 1 : max_line]) return dict.fromkeys(line_range, joined) - @functools.cached_property - def _noqa_line_mapping(self) -> dict[int, str]: - """Map from line number to the line we'll search for `noqa` in.""" - try: - file_tokens = self.file_tokens - except (tokenize.TokenError, SyntaxError): - # if we failed to parse the file tokens, we'll always fail in - # the future, so set this so the code does not try again - return {} - else: - ret = {} - - min_line = len(self.lines) + 2 - max_line = -1 - for tp, _, (s_line, _), (e_line, _), _ in file_tokens: - if tp == tokenize.ENDMARKER or tp == tokenize.DEDENT: - continue - - min_line = min(min_line, s_line) - max_line = max(max_line, e_line) - - if tp in (tokenize.NL, tokenize.NEWLINE): - ret.update(self._noqa_line_range(min_line, max_line)) - - min_line = len(self.lines) + 2 - max_line = -1 - - return ret - def noqa_line_for(self, line_number: int) -> str | None: """Retrieve the line which will be used to determine noqa.""" + if self._noqa_line_mapping is None: + try: + file_tokens = self.file_tokens + except (tokenize.TokenError, SyntaxError): + # if we failed to parse the file tokens, we'll always fail in + # the future, so set this so the code does not try again + self._noqa_line_mapping = {} + else: + ret = {} + + min_line = len(self.lines) + 2 + max_line = -1 + for tp, _, (s_line, _), (e_line, _), _ in file_tokens: + if tp == tokenize.ENDMARKER: + break + + min_line = min(min_line, s_line) + max_line = max(max_line, e_line) + + if tp in (tokenize.NL, tokenize.NEWLINE): + ret.update(self._noqa_line_range(min_line, max_line)) + + min_line = len(self.lines) + 2 + max_line = -1 + + # in newer versions of python, a `NEWLINE` token is inserted + # at the end of the file even if it doesn't have one. + # on old pythons, they will not have hit a `NEWLINE` + if max_line != -1: + ret.update(self._noqa_line_range(min_line, max_line)) + + self._noqa_line_mapping = ret + # NOTE(sigmavirus24): Some plugins choose to report errors for empty # files on Line 1. In those cases, we shouldn't bother trying to # retrieve a physical line (since none exist). @@ -367,7 +365,7 @@ class FileProcessor: elif any(defaults.NOQA_FILE.search(line) for line in self.lines): LOG.warning( "Detected `flake8: noqa` on line with code. To ignore an " - "error on a line use `noqa` instead.", + "error on a line use `noqa` instead." ) return False else: @@ -379,8 +377,12 @@ class FileProcessor: # If we have nothing to analyze quit early return + first_byte = ord(self.lines[0][0]) + if first_byte not in (0xEF, 0xFEFF): + return + # If the first byte of the file is a UTF-8 BOM, strip it - if self.lines[0][:1] == "\uFEFF": + if first_byte == 0xFEFF: self.lines[0] = self.lines[0][1:] elif self.lines[0][:3] == "\xEF\xBB\xBF": self.lines[0] = self.lines[0][3:] @@ -388,12 +390,12 @@ class FileProcessor: def is_eol_token(token: tokenize.TokenInfo) -> bool: """Check if the token is an end-of-line token.""" - return token[0] in NEWLINE or token[4][token[3][1]:].lstrip() == "\\\n" + return token[0] in NEWLINE or token[4][token[3][1] :].lstrip() == "\\\n" def is_multiline_string(token: tokenize.TokenInfo) -> bool: """Check if this is a multiline string.""" - return token.type in {FSTRING_END, TSTRING_END} or ( + return token.type == FSTRING_END or ( token.type == tokenize.STRING and "\n" in token.string ) diff --git a/src/flake8/statistics.py b/src/flake8/statistics.py index b30e4c7..a33e6a6 100644 --- a/src/flake8/statistics.py +++ b/src/flake8/statistics.py @@ -1,7 +1,7 @@ """Statistic collection logic for Flake8.""" from __future__ import annotations -from collections.abc import Generator +from typing import Generator from typing import NamedTuple from flake8.violation import Violation @@ -35,8 +35,8 @@ class Statistics: self._store[key].increment() def statistics_for( - self, prefix: str, filename: str | None = None, - ) -> Generator[Statistic]: + self, prefix: str, filename: str | None = None + ) -> Generator[Statistic, None, None]: """Generate statistics for the prefix and filename. If you have a :class:`Statistics` object that has recorded errors, @@ -108,7 +108,7 @@ class Statistic: """ def __init__( - self, error_code: str, filename: str, message: str, count: int, + self, error_code: str, filename: str, message: str, count: int ) -> None: """Initialize our Statistic.""" self.error_code = error_code diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index d675df7..a409484 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -7,8 +7,8 @@ import copy import enum import functools import logging -from collections.abc import Generator -from collections.abc import Sequence +from typing import Generator +from typing import Sequence from flake8 import defaults from flake8 import statistics @@ -218,18 +218,20 @@ class StyleGuideManager: self.decider = decider or DecisionEngine(options) self.style_guides: list[StyleGuide] = [] self.default_style_guide = StyleGuide( - options, formatter, self.stats, decider=decider, + options, formatter, self.stats, decider=decider ) self.style_guides = [ self.default_style_guide, *self.populate_style_guides_with(options), ] - self.style_guide_for = functools.cache(self._style_guide_for) + self.style_guide_for = functools.lru_cache(maxsize=None)( + self._style_guide_for + ) def populate_style_guides_with( - self, options: argparse.Namespace, - ) -> Generator[StyleGuide]: + self, options: argparse.Namespace + ) -> Generator[StyleGuide, None, None]: """Generate style guides from the per-file-ignores option. :param options: @@ -240,7 +242,7 @@ class StyleGuideManager: per_file = utils.parse_files_to_codes_mapping(options.per_file_ignores) for filename, violations in per_file: yield self.default_style_guide.copy( - filename=filename, extend_ignore_with=violations, + filename=filename, extend_ignore_with=violations ) def _style_guide_for(self, filename: str) -> StyleGuide: @@ -251,7 +253,9 @@ class StyleGuideManager: ) @contextlib.contextmanager - def processing_file(self, filename: str) -> Generator[StyleGuide]: + def processing_file( + self, filename: str + ) -> Generator[StyleGuide, None, None]: """Record the fact that we're processing the file's results.""" guide = self.style_guide_for(filename) with guide.processing_file(filename): @@ -288,7 +292,7 @@ class StyleGuideManager: """ guide = self.style_guide_for(filename) return guide.handle_error( - code, filename, line_number, column_number, text, physical_line, + code, filename, line_number, column_number, text, physical_line ) @@ -330,11 +334,13 @@ class StyleGuide: options.extend_ignore = options.extend_ignore or [] options.extend_ignore.extend(extend_ignore_with or []) return StyleGuide( - options, self.formatter, self.stats, filename=filename, + options, self.formatter, self.stats, filename=filename ) @contextlib.contextmanager - def processing_file(self, filename: str) -> Generator[StyleGuide]: + def processing_file( + self, filename: str + ) -> Generator[StyleGuide, None, None]: """Record the fact that we're processing the file's results.""" self.formatter.beginning(filename) yield self diff --git a/src/flake8/utils.py b/src/flake8/utils.py index e5c086e..afc3896 100644 --- a/src/flake8/utils.py +++ b/src/flake8/utils.py @@ -11,9 +11,9 @@ import re import sys import textwrap import tokenize -from collections.abc import Sequence -from re import Pattern from typing import NamedTuple +from typing import Pattern +from typing import Sequence from flake8 import exceptions @@ -23,7 +23,7 @@ NORMALIZE_PACKAGE_NAME_RE = re.compile(r"[-_.]+") def parse_comma_separated_list( - value: str, regexp: Pattern[str] = COMMA_SEPARATED_LIST_RE, + value: str, regexp: Pattern[str] = COMMA_SEPARATED_LIST_RE ) -> list[str]: """Parse a comma-separated list. @@ -115,7 +115,7 @@ def parse_files_to_codes_mapping( # noqa: C901 f"Expected `per-file-ignores` to be a mapping from file exclude " f"patterns to ignore codes.\n\n" f"Configured `per-file-ignores` setting:\n\n" - f"{textwrap.indent(value.strip(), ' ')}", + f"{textwrap.indent(value.strip(), ' ')}" ) for token in _tokenize_files_to_codes_mapping(value): @@ -150,7 +150,7 @@ def parse_files_to_codes_mapping( # noqa: C901 def normalize_paths( - paths: Sequence[str], parent: str = os.curdir, + paths: Sequence[str], parent: str = os.curdir ) -> list[str]: """Normalize a list of paths relative to a parent directory. diff --git a/src/flake8/violation.py b/src/flake8/violation.py index 8535178..96161d4 100644 --- a/src/flake8/violation.py +++ b/src/flake8/violation.py @@ -4,7 +4,7 @@ from __future__ import annotations import functools import linecache import logging -from re import Match +from typing import Match from typing import NamedTuple from flake8 import defaults @@ -64,6 +64,6 @@ class Violation(NamedTuple): return True LOG.debug( - "%r is not ignored inline with ``# noqa: %s``", self, codes_str, + "%r is not ignored inline with ``# noqa: %s``", self, codes_str ) return False diff --git a/tests/integration/test_checker.py b/tests/integration/test_checker.py index f7f07af..a585f5a 100644 --- a/tests/integration/test_checker.py +++ b/tests/integration/test_checker.py @@ -2,6 +2,7 @@ from __future__ import annotations import importlib.metadata +import sys from unittest import mock import pytest @@ -96,7 +97,7 @@ def mock_file_checker_with_plugin(plugin_target): # Prevent it from reading lines from stdin or somewhere else with mock.patch( - "flake8.processor.FileProcessor.read_lines", return_value=["Line 1"], + "flake8.processor.FileProcessor.read_lines", return_value=["Line 1"] ): file_checker = checker.FileChecker( filename="-", @@ -321,10 +322,17 @@ def test_handling_syntaxerrors_across_pythons(): We need to handle that correctly to avoid crashing. https://github.com/PyCQA/flake8/issues/1372 """ - err = SyntaxError( - "invalid syntax", ("", 2, 1, "bad python:\n", 2, 11), - ) - expected = (2, 1) + if sys.version_info < (3, 10): # pragma: no cover (<3.10) + # Python 3.9 or older + err = SyntaxError( + "invalid syntax", ("", 2, 5, "bad python:\n") + ) + expected = (2, 4) + else: # pragma: no cover (3.10+) + err = SyntaxError( + "invalid syntax", ("", 2, 1, "bad python:\n", 2, 11) + ) + expected = (2, 1) file_checker = checker.FileChecker( filename="-", plugins=finder.Checkers([], [], []), diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index 0ca5b63..68b93cb 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -168,8 +168,10 @@ def test_tokenization_error_but_not_syntax_error(tmpdir, capsys): tmpdir.join("t.py").write("b'foo' \\\n") assert cli.main(["t.py"]) == 1 - if sys.implementation.name == "pypy": # pragma: no cover (pypy) - expected = "t.py:1:9: E999 SyntaxError: unexpected end of file (EOF) in multi-line statement\n" # noqa: E501 + if hasattr(sys, "pypy_version_info"): # pragma: no cover (pypy) + expected = "t.py:2:1: E999 SyntaxError: end of file (EOF) in multi-line statement\n" # noqa: E501 + elif sys.version_info < (3, 10): # pragma: no cover (cp38+) + expected = "t.py:1:8: E999 SyntaxError: unexpected EOF while parsing\n" else: # pragma: no cover (cp310+) expected = "t.py:1:10: E999 SyntaxError: unexpected EOF while parsing\n" # noqa: E501 @@ -184,8 +186,10 @@ def test_tokenization_error_is_a_syntax_error(tmpdir, capsys): tmpdir.join("t.py").write("if True:\n pass\n pass\n") assert cli.main(["t.py"]) == 1 - if sys.implementation.name == "pypy": # pragma: no cover (pypy) - expected = "t.py:3:3: E999 IndentationError: unindent does not match any outer indentation level\n" # noqa: E501 + if hasattr(sys, "pypy_version_info"): # pragma: no cover (pypy) + expected = "t.py:3:2: E999 IndentationError: unindent does not match any outer indentation level\n" # noqa: E501 + elif sys.version_info < (3, 10): # pragma: no cover (= (3, 12): # pragma: >=3.12 cover - expected = """\ -t.py:1:1: T001 "f'xxx{hello}xxxx{world}xxx'" -""" - else: # pragma: <3.12 cover - expected = """\ -t.py:1:1: T001 "f'xxxxxxxxxxxxxxxxxxxxxxxx'" -""" - out, err = capsys.readouterr() - assert out == expected - - -@pytest.mark.xfail(sys.version_info < (3, 14), reason="3.14+") -def test_tstring_logical_line(tmpdir, capsys): # pragma: >=3.14 cover - cfg_s = f"""\ -[flake8] -extend-ignore = F -[flake8:local-plugins] -extension = - T = {yields_logical_line.__module__}:{yields_logical_line.__name__} -""" - - cfg = tmpdir.join("tox.ini") - cfg.write(cfg_s) - - src = """\ -t''' -hello {world} -''' -t'{{"{hello}": "{world}"}}' -""" - t_py = tmpdir.join("t.py") - t_py.write_binary(src.encode()) - - with tmpdir.as_cwd(): - assert main(("t.py", "--config", str(cfg))) == 1 - - expected = """\ -t.py:1:1: T001 "t'''xxxxxxx{world}x'''" -t.py:4:1: T001 "t'xxx{hello}xxxx{world}xxx'" -""" - out, err = capsys.readouterr() - assert out == expected diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py index a155ef1..b289bef 100644 --- a/tests/unit/plugins/finder_test.py +++ b/tests/unit/plugins/finder_test.py @@ -42,7 +42,7 @@ def test_plugins_all_plugins(): logical_line_plugin = _loaded(parameters={"logical_line": True}) physical_line_plugin = _loaded(parameters={"physical_line": True}) report_plugin = _loaded( - plugin=_plugin(ep=_ep(name="R", group="flake8.report")), + plugin=_plugin(ep=_ep(name="R", group="flake8.report")) ) plugins = finder.Plugins( @@ -200,16 +200,14 @@ def test_flake8_plugins(flake8_dist, mock_distribution): "flake8", "9001", importlib.metadata.EntryPoint( - "default", - "flake8.formatting.default:Default", - "flake8.report", + "default", "flake8.formatting.default:Default", "flake8.report" ), ), finder.Plugin( "flake8", "9001", importlib.metadata.EntryPoint( - "pylint", "flake8.formatting.default:Pylint", "flake8.report", + "pylint", "flake8.formatting.default:Pylint", "flake8.report" ), ), } @@ -272,7 +270,7 @@ unrelated = unrelated:main "flake8-foo", "1.2.3", importlib.metadata.EntryPoint( - "Q", "flake8_foo:Plugin", "flake8.extension", + "Q", "flake8_foo:Plugin", "flake8.extension" ), ), finder.Plugin( @@ -306,23 +304,21 @@ unrelated = unrelated:main "flake8", "9001", importlib.metadata.EntryPoint( - "default", - "flake8.formatting.default:Default", - "flake8.report", + "default", "flake8.formatting.default:Default", "flake8.report" ), ), finder.Plugin( "flake8", "9001", importlib.metadata.EntryPoint( - "pylint", "flake8.formatting.default:Pylint", "flake8.report", + "pylint", "flake8.formatting.default:Pylint", "flake8.report" ), ), finder.Plugin( "flake8-foo", "1.2.3", importlib.metadata.EntryPoint( - "foo", "flake8_foo:Formatter", "flake8.report", + "foo", "flake8_foo:Formatter", "flake8.report" ), ), } @@ -489,30 +485,28 @@ def test_find_plugins( "flake8", "9001", importlib.metadata.EntryPoint( - "default", - "flake8.formatting.default:Default", - "flake8.report", + "default", "flake8.formatting.default:Default", "flake8.report" ), ), finder.Plugin( "flake8", "9001", importlib.metadata.EntryPoint( - "pylint", "flake8.formatting.default:Pylint", "flake8.report", + "pylint", "flake8.formatting.default:Pylint", "flake8.report" ), ), finder.Plugin( "flake8-foo", "1.2.3", importlib.metadata.EntryPoint( - "Q", "flake8_foo:Plugin", "flake8.extension", + "Q", "flake8_foo:Plugin", "flake8.extension" ), ), finder.Plugin( "flake8-foo", "1.2.3", importlib.metadata.EntryPoint( - "foo", "flake8_foo:Formatter", "flake8.report", + "foo", "flake8_foo:Formatter", "flake8.report" ), ), finder.Plugin( @@ -524,7 +518,7 @@ def test_find_plugins( "local", "local", importlib.metadata.EntryPoint( - "Y", "mod2:attr", "flake8.extension", + "Y", "mod2:attr", "flake8.extension" ), ), finder.Plugin( @@ -729,7 +723,7 @@ def test_import_plugins_extends_sys_path(): def test_classify_plugins(): report_plugin = _loaded( - plugin=_plugin(ep=_ep(name="R", group="flake8.report")), + plugin=_plugin(ep=_ep(name="R", group="flake8.report")) ) tree_plugin = _loaded(parameters={"tree": True}) logical_line_plugin = _loaded(parameters={"logical_line": True}) diff --git a/tests/unit/plugins/reporter_test.py b/tests/unit/plugins/reporter_test.py index 48b2873..ff4d97f 100644 --- a/tests/unit/plugins/reporter_test.py +++ b/tests/unit/plugins/reporter_test.py @@ -11,7 +11,7 @@ from flake8.plugins import reporter def _opts(**kwargs): - kwargs.setdefault("quiet", 0) + kwargs.setdefault("quiet", 0), kwargs.setdefault("color", "never") kwargs.setdefault("output_file", None) return argparse.Namespace(**kwargs) @@ -25,7 +25,7 @@ def reporters(): "flake8", "123", importlib.metadata.EntryPoint( - name, f"{cls.__module__}:{cls.__name__}", "flake8.report", + name, f"{cls.__module__}:{cls.__name__}", "flake8.report" ), ), cls, @@ -72,5 +72,5 @@ def test_make_formatter_format_string(reporters, caplog): "flake8.plugins.reporter", 30, "'hi %(code)s' is an unknown formatter. Falling back to default.", - ), + ) ] diff --git a/tests/unit/test_application.py b/tests/unit/test_application.py index 3c93085..04147ec 100644 --- a/tests/unit/test_application.py +++ b/tests/unit/test_application.py @@ -36,7 +36,7 @@ def application(): ], ) def test_application_exit_code( - result_count, catastrophic, exit_zero, value, application, + result_count, catastrophic, exit_zero, value, application ): """Verify Application.exit_code returns the correct value.""" application.result_count = result_count diff --git a/tests/unit/test_base_formatter.py b/tests/unit/test_base_formatter.py index 0d81c81..5b57335 100644 --- a/tests/unit/test_base_formatter.py +++ b/tests/unit/test_base_formatter.py @@ -50,7 +50,7 @@ def test_format_needs_to_be_implemented(): formatter = base.BaseFormatter(options()) with pytest.raises(NotImplementedError): formatter.format( - Violation("A000", "file.py", 1, 1, "error text", None), + Violation("A000", "file.py", 1, 1, "error text", None) ) @@ -59,7 +59,7 @@ def test_show_source_returns_nothing_when_not_showing_source(): formatter = base.BaseFormatter(options(show_source=False)) assert ( formatter.show_source( - Violation("A000", "file.py", 1, 1, "error text", "line"), + Violation("A000", "file.py", 1, 1, "error text", "line") ) == "" ) @@ -70,7 +70,7 @@ def test_show_source_returns_nothing_when_there_is_source(): formatter = base.BaseFormatter(options(show_source=True)) assert ( formatter.show_source( - Violation("A000", "file.py", 1, 1, "error text", None), + Violation("A000", "file.py", 1, 1, "error text", None) ) == "" ) diff --git a/tests/unit/test_checker_manager.py b/tests/unit/test_checker_manager.py index eecba3b..68dd82a 100644 --- a/tests/unit/test_checker_manager.py +++ b/tests/unit/test_checker_manager.py @@ -41,11 +41,9 @@ def test_oserrors_are_reraised(): err = OSError(errno.EAGAIN, "Ominous message") with mock.patch("_multiprocessing.SemLock", side_effect=err): manager = _parallel_checker_manager() - with ( - mock.patch.object(manager, "run_serial") as serial, - pytest.raises(OSError), - ): - manager.run() + with mock.patch.object(manager, "run_serial") as serial: + with pytest.raises(OSError): + manager.run() assert serial.call_count == 0 @@ -63,20 +61,14 @@ def test_multiprocessing_cpu_count_not_implemented(): assert manager.jobs == 0 -def test_jobs_count_limited_to_file_count(): - style_guide = style_guide_mock() - style_guide.options.jobs = JobsArgument("4") - style_guide.options.filenames = ["file1", "file2"] - manager = checker.Manager(style_guide, finder.Checkers([], [], []), []) - assert manager.jobs == 4 - manager.start() - assert manager.jobs == 2 - - def test_make_checkers(): """Verify that we create a list of FileChecker instances.""" style_guide = style_guide_mock() style_guide.options.filenames = ["file1", "file2"] manager = checker.Manager(style_guide, finder.Checkers([], [], []), []) - manager.start() + + with mock.patch("flake8.utils.fnmatch", return_value=True): + with mock.patch("flake8.processor.FileProcessor"): + manager.start() + assert manager.filenames == ("file1", "file2") diff --git a/tests/unit/test_debug.py b/tests/unit/test_debug.py index 298b598..4ba604f 100644 --- a/tests/unit/test_debug.py +++ b/tests/unit/test_debug.py @@ -14,7 +14,7 @@ def test_debug_information(): pkg, version, importlib.metadata.EntryPoint( - ep_name, "dne:dne", "flake8.extension", + ep_name, "dne:dne", "flake8.extension" ), ), None, diff --git a/tests/unit/test_decision_engine.py b/tests/unit/test_decision_engine.py index cd8f80d..d543d5e 100644 --- a/tests/unit/test_decision_engine.py +++ b/tests/unit/test_decision_engine.py @@ -35,7 +35,7 @@ def create_options(**kwargs): def test_was_ignored_ignores_errors(ignore_list, extend_ignore, error_code): """Verify we detect users explicitly ignoring an error.""" decider = style_guide.DecisionEngine( - create_options(ignore=ignore_list, extend_ignore=extend_ignore), + create_options(ignore=ignore_list, extend_ignore=extend_ignore) ) assert decider.was_ignored(error_code) is style_guide.Ignored.Explicitly @@ -53,11 +53,11 @@ def test_was_ignored_ignores_errors(ignore_list, extend_ignore, error_code): ], ) def test_was_ignored_implicitly_selects_errors( - ignore_list, extend_ignore, error_code, + ignore_list, extend_ignore, error_code ): """Verify we detect users does not explicitly ignore an error.""" decider = style_guide.DecisionEngine( - create_options(ignore=ignore_list, extend_ignore=extend_ignore), + create_options(ignore=ignore_list, extend_ignore=extend_ignore) ) assert decider.was_ignored(error_code) is style_guide.Selected.Implicitly @@ -179,7 +179,7 @@ def test_was_selected_excludes_errors(select_list, error_code): ], ) def test_decision_for( - select_list, ignore_list, extend_ignore, error_code, expected, + select_list, ignore_list, extend_ignore, error_code, expected ): """Verify we decide when to report an error.""" decider = style_guide.DecisionEngine( @@ -187,7 +187,7 @@ def test_decision_for( select=select_list, ignore=ignore_list, extend_ignore=extend_ignore, - ), + ) ) assert decider.decision_for(error_code) is expected diff --git a/tests/unit/test_discover_files.py b/tests/unit/test_discover_files.py index ea55ccc..ca945c2 100644 --- a/tests/unit/test_discover_files.py +++ b/tests/unit/test_discover_files.py @@ -47,7 +47,7 @@ def test_filenames_from_a_directory_with_a_predicate(): _filenames_from( arg=_normpath("a/b/"), predicate=lambda path: path.endswith(_normpath("b/c.py")), - ), + ) ) # should not include c.py expected = _normpaths(("a/b/d.py", "a/b/e/f.py")) @@ -61,7 +61,7 @@ def test_filenames_from_a_directory_with_a_predicate_from_the_current_dir(): _filenames_from( arg=_normpath("./a/b"), predicate=lambda path: path == "c.py", - ), + ) ) # none should have matched the predicate so all returned expected = _normpaths(("./a/b/c.py", "./a/b/d.py", "./a/b/e/f.py")) @@ -132,7 +132,7 @@ def _expand_paths( stdin_display_name=stdin_display_name, filename_patterns=filename_patterns, exclude=exclude, - ), + ) ) diff --git a/tests/unit/test_file_processor.py b/tests/unit/test_file_processor.py index 22c5bcf..a90c628 100644 --- a/tests/unit/test_file_processor.py +++ b/tests/unit/test_file_processor.py @@ -28,7 +28,7 @@ def _lines_from_file(tmpdir, contents, options): def test_read_lines_universal_newlines(tmpdir, default_options): r"""Verify that line endings are translated to \n.""" lines = _lines_from_file( - tmpdir, b"# coding: utf-8\r\nx = 1\r\n", default_options, + tmpdir, b"# coding: utf-8\r\nx = 1\r\n", default_options ) assert lines == ["# coding: utf-8\n", "x = 1\n"] @@ -36,7 +36,7 @@ def test_read_lines_universal_newlines(tmpdir, default_options): def test_read_lines_incorrect_utf_16(tmpdir, default_options): """Verify that an incorrectly encoded file is read as latin-1.""" lines = _lines_from_file( - tmpdir, b"# coding: utf16\nx = 1\n", default_options, + tmpdir, b"# coding: utf16\nx = 1\n", default_options ) assert lines == ["# coding: utf16\n", "x = 1\n"] @@ -44,7 +44,7 @@ def test_read_lines_incorrect_utf_16(tmpdir, default_options): def test_read_lines_unknown_encoding(tmpdir, default_options): """Verify that an unknown encoding is still read as latin-1.""" lines = _lines_from_file( - tmpdir, b"# coding: fake-encoding\nx = 1\n", default_options, + tmpdir, b"# coding: fake-encoding\nx = 1\n", default_options ) assert lines == ["# coding: fake-encoding\n", "x = 1\n"] @@ -289,7 +289,7 @@ def test_processor_split_line(default_options): def test_build_ast(default_options): """Verify the logic for how we build an AST for plugins.""" file_processor = processor.FileProcessor( - "-", default_options, lines=["a = 1\n"], + "-", default_options, lines=["a = 1\n"] ) module = file_processor.build_ast() @@ -299,7 +299,7 @@ def test_build_ast(default_options): def test_next_logical_line_updates_the_previous_logical_line(default_options): """Verify that we update our tracking of the previous logical line.""" file_processor = processor.FileProcessor( - "-", default_options, lines=["a = 1\n"], + "-", default_options, lines=["a = 1\n"] ) file_processor.indent_level = 1 @@ -315,7 +315,7 @@ def test_next_logical_line_updates_the_previous_logical_line(default_options): def test_visited_new_blank_line(default_options): """Verify we update the number of blank lines seen.""" file_processor = processor.FileProcessor( - "-", default_options, lines=["a = 1\n"], + "-", default_options, lines=["a = 1\n"] ) assert file_processor.blank_lines == 0 diff --git a/tests/unit/test_main_options.py b/tests/unit/test_main_options.py index 0b1fb69..7c1feba 100644 --- a/tests/unit/test_main_options.py +++ b/tests/unit/test_main_options.py @@ -6,7 +6,7 @@ from flake8.main import options def test_stage1_arg_parser(): stage1_parser = options.stage1_arg_parser() opts, args = stage1_parser.parse_known_args( - ["--foo", "--verbose", "src", "setup.py", "--statistics", "--version"], + ["--foo", "--verbose", "src", "setup.py", "--statistics", "--version"] ) assert opts.verbose diff --git a/tests/unit/test_option_manager.py b/tests/unit/test_option_manager.py index 9904a2e..92266f3 100644 --- a/tests/unit/test_option_manager.py +++ b/tests/unit/test_option_manager.py @@ -122,7 +122,7 @@ def test_parse_args_handles_comma_separated_defaults(optmanager): assert optmanager.config_options_dict == {} optmanager.add_option( - "--exclude", default="E123,W234", comma_separated_list=True, + "--exclude", default="E123,W234", comma_separated_list=True ) options = optmanager.parse_args([]) @@ -135,7 +135,7 @@ def test_parse_args_handles_comma_separated_lists(optmanager): assert optmanager.config_options_dict == {} optmanager.add_option( - "--exclude", default="E123,W234", comma_separated_list=True, + "--exclude", default="E123,W234", comma_separated_list=True ) options = optmanager.parse_args(["--exclude", "E201,W111,F280"]) @@ -148,11 +148,11 @@ def test_parse_args_normalize_paths(optmanager): assert optmanager.config_options_dict == {} optmanager.add_option( - "--extra-config", normalize_paths=True, comma_separated_list=True, + "--extra-config", normalize_paths=True, comma_separated_list=True ) options = optmanager.parse_args( - ["--extra-config", "../config.ini,tox.ini,flake8/some-other.cfg"], + ["--extra-config", "../config.ini,tox.ini,flake8/some-other.cfg"] ) assert options.extra_config == [ os.path.abspath("../config.ini"), diff --git a/tests/unit/test_options_config.py b/tests/unit/test_options_config.py index d73f471..7de58f0 100644 --- a/tests/unit/test_options_config.py +++ b/tests/unit/test_options_config.py @@ -169,7 +169,7 @@ def test_load_extra_config_utf8(tmpdir): @pytest.fixture def opt_manager(): ret = OptionManager( - version="123", plugin_versions="", parents=[], formatter_names=[], + version="123", plugin_versions="", parents=[], formatter_names=[] ) register_default_options(ret) return ret @@ -213,7 +213,7 @@ def test_parse_config_ignores_unknowns(tmp_path, opt_manager, caplog): "flake8.options.config", 10, 'Option "wat" is not registered. Ignoring.', - ), + ) ] diff --git a/tests/unit/test_style_guide.py b/tests/unit/test_style_guide.py index c66cfd2..94fcb26 100644 --- a/tests/unit/test_style_guide.py +++ b/tests/unit/test_style_guide.py @@ -36,7 +36,7 @@ def test_handle_error_does_not_raise_type_errors(): ) assert 1 == guide.handle_error( - "T111", "file.py", 1, 1, "error found", "a = 1", + "T111", "file.py", 1, 1, "error found", "a = 1" ) @@ -110,7 +110,7 @@ def test_style_guide_manager_pre_file_ignores_parsing(): ], ) def test_style_guide_manager_pre_file_ignores( - ignores, violation, filename, handle_error_return, + ignores, violation, filename, handle_error_return ): """Verify how the StyleGuideManager creates a default style guide.""" formatter = mock.create_autospec(base.BaseFormatter, instance=True) diff --git a/tox.ini b/tox.ini index 539b5c4..aae002d 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,6 @@ envlist = py,flake8,linters,docs deps = pytest!=3.0.5,!=5.2.3 coverage>=6 - covdefaults commands = coverage run -m pytest {posargs} coverage report