From 7234e0e8d95dd1688d2eb41f48945220ee36f422 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 23 May 2025 15:45:47 -0400 Subject: [PATCH] updates for python 3.14 --- .github/workflows/test.yml | 2 +- pyflakes/checker.py | 21 +++++++++++++++++++-- pyflakes/messages.py | 4 ++++ pyflakes/test/test_imports.py | 6 +++++- pyflakes/test/test_other.py | 16 ++++++++++++++++ pyflakes/test/test_type_annotations.py | 25 ++++++++++++++++++++++--- 6 files changed, 67 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d630de6..66c111a2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "pypy-3.9"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14-dev", "pypy-3.9"] os: [ubuntu-latest] # Include minimum py3 + maximum py3 + pypy3 on Windows include: diff --git a/pyflakes/checker.py b/pyflakes/checker.py index 0c96af3b..629dacf0 100644 --- a/pyflakes/checker.py +++ b/pyflakes/checker.py @@ -1216,7 +1216,10 @@ def _enter_annotation(self, ann_type=AnnotationState.BARE): def _in_postponed_annotation(self): return ( self._in_annotation == AnnotationState.STRING or - self.annotationsFutureEnabled + ( + self._in_annotation == AnnotationState.BARE and + (self.annotationsFutureEnabled or sys.version_info >= (3, 14)) + ) ) def handleChildren(self, tree, omit=None): @@ -1350,7 +1353,7 @@ def handleAnnotation(self, annotation, node): annotation.col_offset, messages.ForwardAnnotationSyntaxError, )) - elif self.annotationsFutureEnabled: + elif self.annotationsFutureEnabled or sys.version_info >= (3, 14): self.handle_annotation_always_deferred(annotation, node) else: self.handleNode(annotation, node) @@ -1776,6 +1779,20 @@ def JOINEDSTR(self, node): finally: self._in_fstring = orig + def TEMPLATESTR(self, node): + if not any(isinstance(x, ast.Interpolation) for x in node.values): + self.report(messages.TStringMissingPlaceholders, node) + + # similar to f-strings, conversion / etc. flags are parsed as f-strings + # without placeholders + self._in_fstring, orig = True, self._in_fstring + try: + self.handleChildren(node) + finally: + self._in_fstring = orig + + INTERPOLATION = handleChildren + def DICT(self, node): # Complain if there are duplicate keys with different values # If they have the same value it's not going to cause potentially diff --git a/pyflakes/messages.py b/pyflakes/messages.py index 93307e53..405dc72f 100644 --- a/pyflakes/messages.py +++ b/pyflakes/messages.py @@ -266,6 +266,10 @@ class FStringMissingPlaceholders(Message): message = 'f-string is missing placeholders' +class TStringMissingPlaceholders(Message): + message = 't-string is missing placeholders' + + class StringDotFormatExtraPositionalArguments(Message): message = "'...'.format(...) has unused arguments at position(s): %s" diff --git a/pyflakes/test/test_imports.py b/pyflakes/test/test_imports.py index 0fdf96df..c03a5a9f 100644 --- a/pyflakes/test/test_imports.py +++ b/pyflakes/test/test_imports.py @@ -1,3 +1,5 @@ +from sys import version_info + from pyflakes import messages as m from pyflakes.checker import ( FutureImportation, @@ -6,7 +8,7 @@ StarImportation, SubmoduleImportation, ) -from pyflakes.test.harness import TestCase, skip +from pyflakes.test.harness import TestCase, skip, skipIf class TestImportationObject(TestCase): @@ -990,12 +992,14 @@ def test_futureImportUsed(self): assert print_function is not division ''') + @skipIf(version_info >= (3, 14), 'in 3.14+ this is a SyntaxError') def test_futureImportUndefined(self): """Importing undefined names from __future__ fails.""" self.flakes(''' from __future__ import print_statement ''', m.FutureFeatureNotDefined) + @skipIf(version_info >= (3, 14), 'in 3.14+ this is a SyntaxError') def test_futureImportStar(self): """Importing '*' from __future__ fails.""" self.flakes(''' diff --git a/pyflakes/test/test_other.py b/pyflakes/test/test_other.py index fcf95572..5bf18c0e 100644 --- a/pyflakes/test/test_other.py +++ b/pyflakes/test/test_other.py @@ -1766,6 +1766,13 @@ def test_f_string(self): print(f'\x7b4*baz\N{RIGHT CURLY BRACKET}') ''') + @skipIf(version_info < (3, 14), 'new in Python 3.14') + def test_t_string(self): + self.flakes(''' + baz = 0 + tmpl = t'hello {baz}' + ''') + def test_assign_expr(self): """Test PEP 572 assignment expressions are treated as usage / write.""" self.flakes(''' @@ -1837,6 +1844,15 @@ def test_f_string_without_placeholders(self): print(f'{x:>2} {y:>2}') ''') + @skipIf(version_info < (3, 14), 'new in Python 3.14') + def test_t_string_missing_placeholders(self): + self.flakes("t'foo'", m.TStringMissingPlaceholders) + # make sure this does not trigger the f-string placeholder error + self.flakes(''' + x = y = 5 + tmpl = t'{x:0{y}}' + ''') + def test_invalid_dot_format_calls(self): self.flakes(''' '{'.format(1) diff --git a/pyflakes/test/test_type_annotations.py b/pyflakes/test/test_type_annotations.py index 343083e7..f4f8ded5 100644 --- a/pyflakes/test/test_type_annotations.py +++ b/pyflakes/test/test_type_annotations.py @@ -152,6 +152,9 @@ def bar(): """, m.RedefinedWhileUnused) def test_variable_annotations(self): + def undefined_names_before_py314(*, n: int): + return (m.UndefinedName,) * n if version_info < (3, 14) else () + self.flakes(''' name: str age: int @@ -264,7 +267,8 @@ def f(bar: str): pass self.flakes(''' def f(a: A) -> A: pass class A: pass - ''', m.UndefinedName, m.UndefinedName) + ''', *undefined_names_before_py314(n=2)) + self.flakes(''' def f(a: 'A') -> 'A': return a class A: pass @@ -272,7 +276,7 @@ class A: pass self.flakes(''' a: A class A: pass - ''', m.UndefinedName) + ''', *undefined_names_before_py314(n=1)) self.flakes(''' a: 'A' class A: pass @@ -280,7 +284,7 @@ class A: pass self.flakes(''' T: object def f(t: T): pass - ''', m.UndefinedName) + ''', *undefined_names_before_py314(n=1)) self.flakes(''' T: object def g(t: 'T'): pass @@ -410,6 +414,21 @@ def f(t: T): pass def g(t: 'T'): pass ''') + def test_annotations_do_not_define_names_with_future_annotations(self): + self.flakes(''' + from __future__ import annotations + def f(): + x: str + print(x) + ''', m.UndefinedName) + + @skipIf(version_info < (3, 14), 'new in Python 3.14') + def test_postponed_annotations_py314(self): + self.flakes(''' + def f(x: C) -> None: pass + class C: pass + ''') + def test_type_annotation_clobbers_all(self): self.flakes('''\ from typing import TYPE_CHECKING, List