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: 2 additions & 0 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -2946,6 +2946,8 @@ def compute_dependencies(self) -> None:
# Every module implicitly depends on builtins.
if self.id != "builtins":
self.add_dependency("builtins")
if self.tree.uses_template_strings:
self.add_dependency("string.templatelib")

self.check_blockers() # Can fail due to bogus relative imports

Expand Down
4 changes: 4 additions & 0 deletions mypy/fastparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ def __init__(
self.path = path

self.type_ignores: dict[int, list[str]] = {}
self.uses_template_strings = False

# Cache of visit_X methods keyed by type of visited object
self.visitor_cache: dict[type, Callable[[AST | None], Any]] = {}
Expand Down Expand Up @@ -876,6 +877,7 @@ def translate_module_id(self, id: str) -> str:

def visit_Module(self, mod: ast3.Module) -> MypyFile:
self.type_ignores = {}
self.uses_template_strings = False
for ti in mod.type_ignores:
parsed = parse_type_ignore_tag(ti.tag)
if parsed is not None:
Expand All @@ -888,6 +890,7 @@ def visit_Module(self, mod: ast3.Module) -> MypyFile:
ret = MypyFile(body, self.imports, False, ignored_lines=self.type_ignores)
ret.is_stub = self.is_stub
ret.path = self.path
ret.uses_template_strings = self.uses_template_strings
return ret

# --- stmt ---
Expand Down Expand Up @@ -1688,6 +1691,7 @@ def visit_FormattedValue(self, n: ast3.FormattedValue) -> Expression:

# TemplateStr(expr* values)
def visit_TemplateStr(self, n: ast_TemplateStr) -> TemplateStrExpr:
self.uses_template_strings = True
items: list[Expression | tuple[Expression, str, str | None, Expression | None]] = []
for value in n.values:
if isinstance(value, ast_Interpolation): # type: ignore[misc]
Expand Down
4 changes: 4 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ class MypyFile(SymbolNode):
"is_stub",
"is_cache_skeleton",
"is_partial_stub_package",
"uses_template_strings",
"plugin_deps",
"future_import_flags",
"_is_typeshed_file",
Expand Down Expand Up @@ -362,6 +363,8 @@ class MypyFile(SymbolNode):
# (i.e. a partial stub package), for such packages we suppress any missing
# module errors in addition to missing attribute errors.
is_partial_stub_package: bool
# True if module contains at least one t-string (PEP 750 TemplateStr).
uses_template_strings: bool
# Plugin-created dependencies
plugin_deps: dict[str, set[str]]
# Future imports defined in this file. Populated during semantic analysis.
Expand Down Expand Up @@ -394,6 +397,7 @@ def __init__(
self.is_stub = False
self.is_cache_skeleton = False
self.is_partial_stub_package = False
self.uses_template_strings = False
self.future_import_flags = set()
self._is_typeshed_file = None

Expand Down
12 changes: 11 additions & 1 deletion mypy/strconv.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,17 @@ def visit_template_str_expr(self, o: mypy.nodes.TemplateStrExpr) -> str:
items_repr: list[object] = []
for item in o.items:
if isinstance(item, tuple):
items_repr.append(item[0]) # value expression
value_expr, source, conversion, format_spec = item
interpolation: list[object] = [
("Value", [value_expr]),
f"Source({source!r})",
f"Conversion({conversion!r})",
]
if format_spec is None:
interpolation.append("FormatSpec(None)")
else:
interpolation.append(("FormatSpec", [format_spec]))
items_repr.append(("Interpolation", interpolation))
else:
items_repr.append(item)
return self.dump(items_repr, o)
Expand Down
21 changes: 21 additions & 0 deletions test-data/unit/check-python314.test
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ a = t"foobar"
a = t"{'foobar'}"
[builtins fixtures/f_string.pyi]

[case testTemplateStringWithoutExplicitImport]
reveal_type(t"implicit import works") # N: Revealed type is "string.templatelib.Template"
[builtins fixtures/f_string.pyi]

[case testTemplateStringExpressionsOk]
t".{1 + 1}."
t".{1 + 1}.{'foo' + 'bar'}"
Expand All @@ -30,3 +34,20 @@ width = 10
precision = 4
t"result: {value:{width}.{precision}}"
[builtins fixtures/f_string.pyi]

[case testTemplateStringNestedExpressionsTypeChecked]
t"{2:{3 + ''}}" # E: Unsupported operand types for + ("int" and "str")
[builtins fixtures/f_string.pyi]

[case testIncrementalTemplateStringImplicitDependency]
import m
reveal_type(m.x)
[file m.py]
x = "foo"
[file m.py.2]
x = t"foo"
[out1]
main:2: note: Revealed type is "builtins.str"
[out2]
main:2: note: Revealed type is "string.templatelib.Template"
[builtins fixtures/f_string.pyi]
17 changes: 17 additions & 0 deletions test-data/unit/fine-grained-python314.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[case testTemplateStringImplicitDependencyFineGrained]
import m
x: str = m.x

[file m.py]
x = "foo"

[file m.py.2]
x = t"foo"

[file typing_extensions.pyi]
LiteralString = str

[builtins fixtures/f_string.pyi]
[out]
==
main:2: error: Incompatible types in assignment (expression has type "Template", variable has type "str")
79 changes: 74 additions & 5 deletions test-data/unit/parse-python314.test
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,37 @@ MypyFile:1(
ExpressionStmt:2(
TemplateStrExpr:2(
StrExpr(Hello )
NameExpr(x))))
Interpolation(
Value(
NameExpr(x))
Source('x')
Conversion(None)
FormatSpec(None)))))

[case testTemplateStringWithComplexExpression]
x = 1
y = 2
t'Hello {x + y}'
[out]
MypyFile:1(
AssignmentStmt:1(
NameExpr(x)
IntExpr(1))
AssignmentStmt:2(
NameExpr(y)
IntExpr(2))
ExpressionStmt:3(
TemplateStrExpr:3(
StrExpr(Hello )
Interpolation(
Value(
OpExpr:3(
+
NameExpr(x)
NameExpr(y)))
Source('x + y')
Conversion(None)
FormatSpec(None)))))

[case testTemplateStringWithConversion]
x = 'mypy'
Expand All @@ -22,7 +52,12 @@ MypyFile:1(
ExpressionStmt:2(
TemplateStrExpr:2(
StrExpr(Hello )
NameExpr(x))))
Interpolation(
Value(
NameExpr(x))
Source('x')
Conversion('r')
FormatSpec(None)))))

[case testTemplateStringWithOnlyFormatSpecifier]
x = 'mypy'
Expand All @@ -35,7 +70,13 @@ MypyFile:1(
ExpressionStmt:2(
TemplateStrExpr:2(
StrExpr(Hello )
NameExpr(x))))
Interpolation(
Value(
NameExpr(x))
Source('x')
Conversion(None)
FormatSpec(
StrExpr(<30))))))

[case testTemplateStringWithFormatSpecifierAndConversion]
x = 'mypy'
Expand All @@ -48,7 +89,13 @@ MypyFile:1(
ExpressionStmt:2(
TemplateStrExpr:2(
StrExpr(Hello )
NameExpr(x))))
Interpolation(
Value(
NameExpr(x))
Source('x')
Conversion('s')
FormatSpec(
StrExpr(<30))))))

[case testTemplateStringWithFormatSpecifierExpression]
x = 'mypy'
Expand All @@ -65,4 +112,26 @@ MypyFile:1(
ExpressionStmt:3(
TemplateStrExpr:3(
StrExpr(Hello )
NameExpr(x))))
Interpolation(
Value(
NameExpr(x))
Source('x')
Conversion('s')
FormatSpec(
CallExpr:3(
MemberExpr:3(
StrExpr()
join)
Args(
ListExpr:3(
StrExpr(<)
CallExpr:3(
MemberExpr:3(
StrExpr({:{}})
format)
Args(
OpExpr:3(
+
NameExpr(y)
NameExpr(y))
StrExpr()))))))))))
Loading