Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 19 additions & 2 deletions pyflakes/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
)
Comment on lines 1218 to +1222
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there was a subtle bug here with the existing code -- I added a test for it below

)

def handleChildren(self, tree, omit=None):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions pyflakes/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
6 changes: 5 additions & 1 deletion pyflakes/test/test_imports.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from sys import version_info

from pyflakes import messages as m
from pyflakes.checker import (
FutureImportation,
Expand All @@ -6,7 +8,7 @@
StarImportation,
SubmoduleImportation,
)
from pyflakes.test.harness import TestCase, skip
from pyflakes.test.harness import TestCase, skip, skipIf


class TestImportationObject(TestCase):
Expand Down Expand Up @@ -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('''
Expand Down
16 changes: 16 additions & 0 deletions pyflakes/test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('''
Expand Down Expand Up @@ -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)
Expand Down
25 changes: 22 additions & 3 deletions pyflakes/test/test_type_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -264,23 +267,24 @@ 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
''')
self.flakes('''
a: A
class A: pass
''', m.UndefinedName)
''', *undefined_names_before_py314(n=1))
self.flakes('''
a: 'A'
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
Expand Down Expand Up @@ -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
Expand Down