Repository: PyCQA/pyflakes
Pull request #842 (2025-05-23 19:47:00)

updates for python 3.14


Patch:
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index 0c96af3..629dacf 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -1216,7 +1216,10 @@ class Checker:
     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 @@ class Checker:
                 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 @@ class Checker:
         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 93307e5..405dc72 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"
 


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9d630de..66c111a 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/test/test_imports.py b/pyflakes/test/test_imports.py
index 0fdf96d..c03a5a9 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 @@ from pyflakes.checker import (
     StarImportation,
     SubmoduleImportation,
 )
-from pyflakes.test.harness import TestCase, skip
+from pyflakes.test.harness import TestCase, skip, skipIf
 
 
 class TestImportationObject(TestCase):
@@ -990,12 +992,14 @@ class Test(TestCase):
         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 fcf9557..5bf18c0 100644
--- a/pyflakes/test/test_other.py
+++ b/pyflakes/test/test_other.py
@@ -1766,6 +1766,13 @@ class TestUnusedAssignment(TestCase):
         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 @@ class TestStringFormatting(TestCase):
             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 343083e..f4f8ded 100644
--- a/pyflakes/test/test_type_annotations.py
+++ b/pyflakes/test/test_type_annotations.py
@@ -152,6 +152,9 @@ class TestTypeAnnotations(TestCase):
         """, 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 @@ class TestTypeAnnotations(TestCase):
         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 TestTypeAnnotations(TestCase):
         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 TestTypeAnnotations(TestCase):
         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 @@ class TestTypeAnnotations(TestCase):
         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