From 3613896bd9051147ffa7fd04ac1a98cbc9e35cf2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Mar 2025 10:05:31 -0400 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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 = {