From f35737a32d8283fd0d3456bd9919718ff2d1b077 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 16 Feb 2025 13:29:05 -0500 Subject: [PATCH 01/12] avoid starting unnecessary processes when file count is limited --- src/flake8/checker.py | 1 + tests/unit/test_checker_manager.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/src/flake8/checker.py b/src/flake8/checker.py index 329a2cc3..ff626796 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -256,6 +256,7 @@ def start(self) -> None: exclude=self.exclude, ) ) + self.jobs = min(len(self.filenames), self.jobs) def stop(self) -> None: """Stop checking files.""" diff --git a/tests/unit/test_checker_manager.py b/tests/unit/test_checker_manager.py index 68dd82aa..8d6b3dcf 100644 --- a/tests/unit/test_checker_manager.py +++ b/tests/unit/test_checker_manager.py @@ -61,6 +61,16 @@ 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() From fffee8ba9dc5903484f99390e6c7f4bbef59bda7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 16 Feb 2025 13:48:15 -0500 Subject: [PATCH 02/12] Release 7.1.2 --- docs/source/release-notes/7.1.2.rst | 15 +++++++++++++++ docs/source/release-notes/index.rst | 1 + src/flake8/__init__.py | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 docs/source/release-notes/7.1.2.rst diff --git a/docs/source/release-notes/7.1.2.rst b/docs/source/release-notes/7.1.2.rst new file mode 100644 index 00000000..010656cc --- /dev/null +++ b/docs/source/release-notes/7.1.2.rst @@ -0,0 +1,15 @@ +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/index.rst b/docs/source/release-notes/index.rst index 9bf8646d..21451791 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -12,6 +12,7 @@ with the newest releases first. 7.0.0 7.1.0 7.1.1 + 7.1.2 6.x Release Series ================== diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index 101eafe7..03454991 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "7.1.1" +__version__ = "7.1.2" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { From fa2ed7145cdf80fd983c2a436226a074f9a9d664 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 16 Feb 2025 15:21:24 -0500 Subject: [PATCH 03/12] remove a few unnecessary mocks in test_checker_manager noticed while implementing the --jobs limiter --- tests/unit/test_checker_manager.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/unit/test_checker_manager.py b/tests/unit/test_checker_manager.py index 8d6b3dcf..593822b6 100644 --- a/tests/unit/test_checker_manager.py +++ b/tests/unit/test_checker_manager.py @@ -76,9 +76,5 @@ def test_make_checkers(): style_guide = style_guide_mock() style_guide.options.filenames = ["file1", "file2"] manager = checker.Manager(style_guide, finder.Checkers([], [], []), []) - - with mock.patch("flake8.utils.fnmatch", return_value=True): - with mock.patch("flake8.processor.FileProcessor"): - manager.start() - + manager.start() assert manager.filenames == ("file1", "file2") From 9d55ccdb729d1255b9cf09438b6073b05b9ce52c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Mar 2025 15:39:58 -0400 Subject: [PATCH 04/12] py39+ --- .github/workflows/main.yml | 10 +++++----- .pre-commit-config.yaml | 10 +++++----- bin/gen-pycodestyle-plugin | 10 +++++----- docs/source/internal/releases.rst | 4 ++-- docs/source/user/invocation.rst | 10 +++++----- example-plugin/setup.py | 2 -- setup.cfg | 2 +- src/flake8/checker.py | 10 ++++------ src/flake8/discover_files.py | 8 ++++---- src/flake8/main/application.py | 2 +- src/flake8/main/cli.py | 2 +- src/flake8/options/aggregator.py | 2 +- src/flake8/options/manager.py | 2 +- src/flake8/options/parse_args.py | 2 +- src/flake8/plugins/finder.py | 12 ++++++------ src/flake8/plugins/pycodestyle.py | 6 +++--- src/flake8/plugins/pyflakes.py | 4 ++-- src/flake8/processor.py | 16 ++++++---------- src/flake8/statistics.py | 4 ++-- src/flake8/style_guide.py | 18 ++++++------------ src/flake8/utils.py | 4 ++-- src/flake8/violation.py | 2 +- 22 files changed, 64 insertions(+), 78 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e2102040..08f54ea6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,9 +15,6 @@ jobs: - os: ubuntu-latest python: pypy-3.9 toxenv: py - - os: ubuntu-latest - python: 3.8 - toxenv: py - os: ubuntu-latest python: 3.9 toxenv: py @@ -28,11 +25,14 @@ jobs: python: '3.11' toxenv: py - os: ubuntu-latest - python: '3.12-dev' + python: '3.12' + toxenv: py + - os: ubuntu-latest + python: '3.13' toxenv: py # windows - os: windows-latest - python: 3.8 + python: 3.9 toxenv: py # misc - os: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad60be1d..9df4a797 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,19 +12,19 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.12.0 + rev: v3.14.0 hooks: - id: reorder-python-imports args: [ --application-directories, '.:src', - --py38-plus, + --py39-plus, --add-import, 'from __future__ import annotations', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.19.1 hooks: - id: pyupgrade - args: [--py38-plus] + args: [--py39-plus] - repo: https://github.com/psf/black rev: 23.12.1 hooks: @@ -35,7 +35,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.15.0 hooks: - id: mypy exclude: ^(docs/|example-plugin/) diff --git a/bin/gen-pycodestyle-plugin b/bin/gen-pycodestyle-plugin index 8bc2efce..c93fbfe1 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 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, None, None]: +def lines() -> Generator[str]: logical = [] physical = [] @@ -58,8 +58,8 @@ def lines() -> Generator[str, None, None]: 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, None, None]: 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], None, None]:" + yield ") -> Generator[tuple[int, str]]:" yield ' """Run pycodestyle logical checks."""' for call in sorted(logical): yield call.to_src() @@ -82,7 +82,7 @@ def lines() -> Generator[str, None, None]: 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], None, None]:" + yield ") -> Generator[tuple[int, str]]:" yield ' """Run pycodestyle physical checks."""' for call in sorted(physical): yield call.to_src() diff --git a/docs/source/internal/releases.rst b/docs/source/internal/releases.rst index 00815097..d71796d4 100644 --- a/docs/source/internal/releases.rst +++ b/docs/source/internal/releases.rst @@ -81,9 +81,9 @@ for users. Before releasing, the following tox test environments must pass: -- Python 3.8 (a.k.a., ``tox -e py38``) +- Python 3.9 (a.k.a., ``tox -e py39``) -- Python 3.12 (a.k.a., ``tox -e py312``) +- Python 3.13 (a.k.a., ``tox -e py313``) - PyPy 3 (a.k.a., ``tox -e pypy3``) diff --git a/docs/source/user/invocation.rst b/docs/source/user/invocation.rst index 61cef977..10895dd2 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.8 and Python 3.9) and you need to call a specific +of Python (e.g., Python 3.13 and Python 3.14) and you need to call a specific version. In that case, you will have much better results using: .. prompt:: bash - python3.8 -m flake8 + python3.13 -m flake8 Or .. prompt:: bash - python3.9 -m flake8 + python3.14 -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.8 and - Python 3.9. It will only install it for the version of Python that + 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 is running pip. It is also possible to specify command-line options directly to |Flake8|: diff --git a/example-plugin/setup.py b/example-plugin/setup.py index c0720bd6..9e7c89f7 100644 --- a/example-plugin/setup.py +++ b/example-plugin/setup.py @@ -23,8 +23,6 @@ "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 28e6f933..688e3490 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,7 @@ install_requires = mccabe>=0.7.0,<0.8.0 pycodestyle>=2.12.0,<2.13.0 pyflakes>=3.2.0,<3.3.0 -python_requires = >=3.8.1 +python_requires = >=3.9 package_dir = =src diff --git a/src/flake8/checker.py b/src/flake8/checker.py index ff626796..d1659b7e 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -9,12 +9,10 @@ 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 @@ -27,7 +25,7 @@ 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__) @@ -53,7 +51,7 @@ @contextlib.contextmanager def _mp_prefork( plugins: Checkers, options: argparse.Namespace -) -> Generator[None, None, None]: +) -> Generator[None]: # we can save significant startup work w/ `fork` multiprocessing global _mp_plugins, _mp_options _mp_plugins, _mp_options = plugins, options diff --git a/src/flake8/discover_files.py b/src/flake8/discover_files.py index 580d5fdf..da28ba5d 100644 --- a/src/flake8/discover_files.py +++ b/src/flake8/discover_files.py @@ -3,9 +3,9 @@ import logging import os.path +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, None, None]: +) -> Generator[str]: """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, None, None]: +) -> Generator[str]: """Expand out ``paths`` from commandline to the lintable files.""" if not paths: paths = ["."] diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index b6bfae37..4704cbd5 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -5,7 +5,7 @@ import json import logging import time -from typing import Sequence +from collections.abc import Sequence import flake8 from flake8 import checker diff --git a/src/flake8/main/cli.py b/src/flake8/main/cli.py index 01a67ac9..1a52f36d 100644 --- a/src/flake8/main/cli.py +++ b/src/flake8/main/cli.py @@ -2,7 +2,7 @@ from __future__ import annotations import sys -from typing import Sequence +from collections.abc import Sequence from flake8.main import application diff --git a/src/flake8/options/aggregator.py b/src/flake8/options/aggregator.py index af8e7449..999161ab 100644 --- a/src/flake8/options/aggregator.py +++ b/src/flake8/options/aggregator.py @@ -8,7 +8,7 @@ import argparse import configparser import logging -from typing import Sequence +from collections.abc import Sequence from flake8.options import config from flake8.options.manager import OptionManager diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index 4fd26b29..cb195fe2 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -5,9 +5,9 @@ import enum import functools import logging +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 diff --git a/src/flake8/options/parse_args.py b/src/flake8/options/parse_args.py index e3f8795f..ff5e08f4 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 typing import Sequence +from collections.abc import Sequence import flake8 from flake8.main import options diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py index 380ec3ac..88b66a08 100644 --- a/src/flake8/plugins/finder.py +++ b/src/flake8/plugins/finder.py @@ -7,9 +7,9 @@ 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, None, None]: + def all_plugins(self) -> Generator[LoadedPlugin]: """Return an iterator over all :class:`LoadedPlugin`s.""" yield from self.checkers.tree yield from self.checkers.logical_line @@ -151,7 +151,7 @@ def _flake8_plugins( eps: Iterable[importlib.metadata.EntryPoint], name: str, version: str, -) -> Generator[Plugin, None, None]: +) -> Generator[Plugin]: pyflakes_meta = importlib.metadata.distribution("pyflakes").metadata pycodestyle_meta = importlib.metadata.distribution("pycodestyle").metadata @@ -173,7 +173,7 @@ def _flake8_plugins( yield Plugin(name, version, ep) -def _find_importlib_plugins() -> Generator[Plugin, None, None]: +def _find_importlib_plugins() -> Generator[Plugin]: # 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, None, None]: def _find_local_plugins( cfg: configparser.RawConfigParser, -) -> Generator[Plugin, None, None]: +) -> Generator[Plugin]: for plugin_type in ("extension", "report"): group = f"flake8.{plugin_type}" for plugin_s in utils.parse_comma_separated_list( diff --git a/src/flake8/plugins/pycodestyle.py b/src/flake8/plugins/pycodestyle.py index 9e1d2bbb..cd760dc6 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], None, None]: +) -> Generator[tuple[int, str]]: """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], None, None]: +) -> Generator[tuple[int, str]]: """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 6c576191..2835e037 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -4,8 +4,8 @@ import argparse import ast import logging +from collections.abc import Generator from typing import Any -from typing import Generator import pyflakes.checker @@ -97,7 +97,7 @@ def parse_options(cls, options: argparse.Namespace) -> None: cls.builtIns = cls.builtIns.union(options.builtins) cls.with_doctest = options.doctests - def run(self) -> Generator[tuple[int, int, str, type[Any]], None, None]: + def run(self) -> Generator[tuple[int, int, str, type[Any]]]: """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 e44547b3..610964dc 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -6,10 +6,8 @@ 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 @@ -24,8 +22,8 @@ [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: @@ -127,9 +125,7 @@ def fstring_start(self, lineno: int) -> None: # pragma: >=3.12 cover """Signal the beginning of an fstring.""" self._fstring_start = lineno - def multiline_string( - self, token: tokenize.TokenInfo - ) -> Generator[str, None, None]: + def multiline_string(self, token: tokenize.TokenInfo) -> Generator[str]: """Iterate through the lines of a multiline string.""" if token.type == FSTRING_END: # pragma: >=3.12 cover start = self._fstring_start @@ -210,7 +206,7 @@ def build_logical_line_tokens(self) -> _Logical: # noqa: C901 brace_offset = text.count("{") + text.count("}") text = "x" * (len(text) + brace_offset) end = (end[0], end[1] + brace_offset) - if previous_row: + if previous_row is not None and previous_column is not None: (start_row, start_column) = start if previous_row != start_row: row_index = previous_row - 1 @@ -263,7 +259,7 @@ def keyword_arguments_for( ) return ret - def generate_tokens(self) -> Generator[tokenize.TokenInfo, None, None]: + def generate_tokens(self) -> Generator[tokenize.TokenInfo]: """Tokenize the file and yield the tokens.""" for token in tokenize.generate_tokens(self.next_line): if token[2][0] > self.total_lines: diff --git a/src/flake8/statistics.py b/src/flake8/statistics.py index a33e6a64..5a222548 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 typing import Generator +from collections.abc import Generator from typing import NamedTuple from flake8.violation import Violation @@ -36,7 +36,7 @@ def record(self, error: Violation) -> None: def statistics_for( self, prefix: str, filename: str | None = None - ) -> Generator[Statistic, None, None]: + ) -> Generator[Statistic]: """Generate statistics for the prefix and filename. If you have a :class:`Statistics` object that has recorded errors, diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index a4094840..f72e6d87 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -7,8 +7,8 @@ import enum import functools import logging -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from flake8 import defaults from flake8 import statistics @@ -225,13 +225,11 @@ def __init__( *self.populate_style_guides_with(options), ] - self.style_guide_for = functools.lru_cache(maxsize=None)( - self._style_guide_for - ) + self.style_guide_for = functools.cache(self._style_guide_for) def populate_style_guides_with( self, options: argparse.Namespace - ) -> Generator[StyleGuide, None, None]: + ) -> Generator[StyleGuide]: """Generate style guides from the per-file-ignores option. :param options: @@ -253,9 +251,7 @@ def _style_guide_for(self, filename: str) -> StyleGuide: ) @contextlib.contextmanager - def processing_file( - self, filename: str - ) -> Generator[StyleGuide, None, None]: + def processing_file(self, filename: str) -> Generator[StyleGuide]: """Record the fact that we're processing the file's results.""" guide = self.style_guide_for(filename) with guide.processing_file(filename): @@ -338,9 +334,7 @@ def copy( ) @contextlib.contextmanager - def processing_file( - self, filename: str - ) -> Generator[StyleGuide, None, None]: + def processing_file(self, filename: str) -> Generator[StyleGuide]: """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 afc3896d..67db33fb 100644 --- a/src/flake8/utils.py +++ b/src/flake8/utils.py @@ -11,9 +11,9 @@ 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 diff --git a/src/flake8/violation.py b/src/flake8/violation.py index 96161d4e..ae1631ac 100644 --- a/src/flake8/violation.py +++ b/src/flake8/violation.py @@ -4,7 +4,7 @@ import functools import linecache import logging -from typing import Match +from re import Match from typing import NamedTuple from flake8 import defaults From d56d569ce40a623a17c212ea7f2b306714f27f31 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Mar 2025 15:53:41 -0400 Subject: [PATCH 05/12] update versions of pycodestyle / pyflakes --- setup.cfg | 4 ++-- src/flake8/plugins/pyflakes.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 688e3490..6f63f5a6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,8 +29,8 @@ classifiers = packages = find: install_requires = mccabe>=0.7.0,<0.8.0 - pycodestyle>=2.12.0,<2.13.0 - pyflakes>=3.2.0,<3.3.0 + pycodestyle>=2.13.0,<2.14.0 + pyflakes>=3.3.0,<3.4.0 python_requires = >=3.9 package_dir = =src diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index 2835e037..3620a27b 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -55,6 +55,7 @@ "UndefinedName": "F821", "UndefinedExport": "F822", "UndefinedLocal": "F823", + "UnusedIndirectAssignment": "F824", "DuplicateArgument": "F831", "UnusedVariable": "F841", "UnusedAnnotation": "F842", From 16f5f28a384f0781bebb37a08aa45e65b9526c50 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Mar 2025 16:17:35 -0400 Subject: [PATCH 06/12] Release 7.2.0 --- docs/source/release-notes/7.2.0.rst | 19 +++++++++++++++++++ docs/source/release-notes/index.rst | 1 + src/flake8/__init__.py | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 docs/source/release-notes/7.2.0.rst diff --git a/docs/source/release-notes/7.2.0.rst b/docs/source/release-notes/7.2.0.rst new file mode 100644 index 00000000..fe124d7b --- /dev/null +++ b/docs/source/release-notes/7.2.0.rst @@ -0,0 +1,19 @@ +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/index.rst b/docs/source/release-notes/index.rst index 21451791..a4d8bfca 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -13,6 +13,7 @@ with the newest releases first. 7.1.0 7.1.1 7.1.2 + 7.2.0 6.x Release Series ================== diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index 03454991..cf91f8b8 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "7.1.2" +__version__ = "7.2.0" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = { From 3613896bd9051147ffa7fd04ac1a98cbc9e35cf2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Mar 2025 10:05:31 -0400 Subject: [PATCH 07/12] document F824 --- docs/source/user/error-codes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/user/error-codes.rst b/docs/source/user/error-codes.rst index 2a914132..3090d473 100644 --- a/docs/source/user/error-codes.rst +++ b/docs/source/user/error-codes.rst @@ -102,6 +102,9 @@ 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 | From 8dfa6695b4fb1e1401b357367a0a71037d29f6aa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 11 Apr 2025 17:39:39 -0400 Subject: [PATCH 08/12] add rtd sphinx config --- .readthedocs.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 0425dc24..dfa8b9dc 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,3 +8,5 @@ python: install: - path: . - requirements: docs/source/requirements.txt +sphinx: + configuration: docs/source/conf.py From 019424b80d3d7d5d8a2a1638f5877080546e3f46 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 23 May 2025 16:25:06 -0400 Subject: [PATCH 09/12] add support for t-strings --- src/flake8/_compat.py | 7 +++++++ src/flake8/checker.py | 3 +++ src/flake8/processor.py | 17 +++++++++++++--- tests/integration/test_plugins.py | 33 +++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/flake8/_compat.py b/src/flake8/_compat.py index e8a3ccd0..22bb84eb 100644 --- a/src/flake8/_compat.py +++ b/src/flake8/_compat.py @@ -9,3 +9,10 @@ FSTRING_END = tokenize.FSTRING_END else: # pragma: <3.12 cover 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/checker.py b/src/flake8/checker.py index d1659b7e..84d45aaa 100644 --- a/src/flake8/checker.py +++ b/src/flake8/checker.py @@ -19,6 +19,7 @@ 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 @@ -554,6 +555,8 @@ def check_physical_eol( 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 diff --git a/src/flake8/processor.py b/src/flake8/processor.py index 610964dc..ccb4c57e 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -13,6 +13,8 @@ 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__) @@ -113,7 +115,7 @@ def __init__( self.verbose = options.verbose #: Statistics dictionary self.statistics = {"logical lines": 0} - self._fstring_start = -1 + self._fstring_start = self._tstring_start = -1 @functools.cached_property def file_tokens(self) -> list[tokenize.TokenInfo]: @@ -125,10 +127,16 @@ def fstring_start(self, lineno: int) -> None: # pragma: >=3.12 cover """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]: """Iterate through the lines of a multiline string.""" if token.type == FSTRING_END: # pragma: >=3.12 cover start = self._fstring_start + elif token.type == TSTRING_END: # pragma: >=3.14 cover + start = self._tstring_start else: start = token.start[0] @@ -198,7 +206,10 @@ def build_logical_line_tokens(self) -> _Logical: # noqa: C901 continue if token_type == tokenize.STRING: text = mutate_string(text) - elif token_type == FSTRING_MIDDLE: # pragma: >=3.12 cover + 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 @@ -382,7 +393,7 @@ def is_eol_token(token: tokenize.TokenInfo) -> bool: def is_multiline_string(token: tokenize.TokenInfo) -> bool: """Check if this is a multiline string.""" - return token.type == FSTRING_END or ( + return token.type in {FSTRING_END, TSTRING_END} or ( token.type == tokenize.STRING and "\n" in token.string ) diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index 90ca555a..471cab89 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -296,3 +296,36 @@ def test_escaping_of_fstrings_in_string_redacter(tmpdir, capsys): """ 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 From 4941a3e32e54488698ecbc23993bfeb2a60c0fc5 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 20 Jun 2025 15:15:53 -0400 Subject: [PATCH 10/12] upgrade pyflakes / pycodestyle --- setup.cfg | 4 ++-- src/flake8/plugins/pyflakes.py | 1 + tests/unit/test_checker_manager.py | 8 +++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index 6f63f5a6..a6b5a5ef 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,8 +29,8 @@ classifiers = packages = find: install_requires = mccabe>=0.7.0,<0.8.0 - pycodestyle>=2.13.0,<2.14.0 - pyflakes>=3.3.0,<3.4.0 + pycodestyle>=2.14.0,<2.15.0 + pyflakes>=3.4.0,<3.5.0 python_requires = >=3.9 package_dir = =src diff --git a/src/flake8/plugins/pyflakes.py b/src/flake8/plugins/pyflakes.py index 3620a27b..66d8c1cd 100644 --- a/src/flake8/plugins/pyflakes.py +++ b/src/flake8/plugins/pyflakes.py @@ -36,6 +36,7 @@ "StringDotFormatMissingArgument": "F524", "StringDotFormatMixingAutomatic": "F525", "FStringMissingPlaceholders": "F541", + "TStringMissingPlaceholders": "F542", "MultiValueRepeatedKeyLiteral": "F601", "MultiValueRepeatedKeyVariable": "F602", "TooManyExpressionsInStarredAssignment": "F621", diff --git a/tests/unit/test_checker_manager.py b/tests/unit/test_checker_manager.py index 593822b6..eecba3b1 100644 --- a/tests/unit/test_checker_manager.py +++ b/tests/unit/test_checker_manager.py @@ -41,9 +41,11 @@ 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: - with pytest.raises(OSError): - manager.run() + with ( + mock.patch.object(manager, "run_serial") as serial, + pytest.raises(OSError), + ): + manager.run() assert serial.call_count == 0 From 6bcdb628597fa2d03494965089ff87a492ffc1e9 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 20 Jun 2025 15:21:27 -0400 Subject: [PATCH 11/12] document F542 --- docs/source/user/error-codes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/user/error-codes.rst b/docs/source/user/error-codes.rst index 3090d473..c8b46c1c 100644 --- a/docs/source/user/error-codes.rst +++ b/docs/source/user/error-codes.rst @@ -59,6 +59,8 @@ 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 | +------+---------------------------------------------------------------------+ From c48217e1fc006c2dddd14df54e83b67da15de5cd Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 20 Jun 2025 15:30:19 -0400 Subject: [PATCH 12/12] Release 7.3.0 --- docs/source/release-notes/7.3.0.rst | 15 +++++++++++++++ docs/source/release-notes/index.rst | 11 ++++++----- src/flake8/__init__.py | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 docs/source/release-notes/7.3.0.rst diff --git a/docs/source/release-notes/7.3.0.rst b/docs/source/release-notes/7.3.0.rst new file mode 100644 index 00000000..dedc918e --- /dev/null +++ b/docs/source/release-notes/7.3.0.rst @@ -0,0 +1,15 @@ +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 a4d8bfca..10697dfb 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -9,18 +9,19 @@ with the newest releases first. ================== .. toctree:: - 7.0.0 - 7.1.0 - 7.1.1 - 7.1.2 + 7.3.0 7.2.0 + 7.1.2 + 7.1.1 + 7.1.0 + 7.0.0 6.x Release Series ================== .. toctree:: - 6.0.0 6.1.0 + 6.0.0 5.x Release Series ================== diff --git a/src/flake8/__init__.py b/src/flake8/__init__.py index cf91f8b8..db291665 100644 --- a/src/flake8/__init__.py +++ b/src/flake8/__init__.py @@ -17,7 +17,7 @@ LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "7.2.0" +__version__ = "7.3.0" __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) _VERBOSITY_TO_LOG_LEVEL = {