Skip to content

Commit 3dbf139

Browse files
authored
Implement inference for JoinedStr and FormattedValue (#2459)
1 parent 3bcf5ec commit 3dbf139

File tree

3 files changed

+91
-0
lines changed

3 files changed

+91
-0
lines changed

ChangeLog

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ Release date: TBA
1717

1818
Closes pylint-dev/pylint#7126
1919

20+
* Implement inference for JoinedStr and FormattedValue
21+
22+
2023

2124
What's New in astroid 3.2.5?
2225
============================

astroid/nodes/node_classes.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4673,6 +4673,37 @@ def get_children(self):
46734673
if self.format_spec is not None:
46744674
yield self.format_spec
46754675

4676+
def _infer(
4677+
self, context: InferenceContext | None = None, **kwargs: Any
4678+
) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
4679+
if self.format_spec is None:
4680+
yield from self.value.infer(context, **kwargs)
4681+
return
4682+
uninferable_already_generated = False
4683+
for format_spec in self.format_spec.infer(context, **kwargs):
4684+
if not isinstance(format_spec, Const):
4685+
if not uninferable_already_generated:
4686+
yield util.Uninferable
4687+
uninferable_already_generated = True
4688+
continue
4689+
for value in self.value.infer(context, **kwargs):
4690+
if not isinstance(value, Const):
4691+
if not uninferable_already_generated:
4692+
yield util.Uninferable
4693+
uninferable_already_generated = True
4694+
continue
4695+
formatted = format(value.value, format_spec.value)
4696+
yield Const(
4697+
formatted,
4698+
lineno=self.lineno,
4699+
col_offset=self.col_offset,
4700+
end_lineno=self.end_lineno,
4701+
end_col_offset=self.end_col_offset,
4702+
)
4703+
4704+
4705+
MISSING_VALUE = "{MISSING_VALUE}"
4706+
46764707

46774708
class JoinedStr(NodeNG):
46784709
"""Represents a list of string expressions to be joined.
@@ -4734,6 +4765,34 @@ def postinit(self, values: list[NodeNG] | None = None) -> None:
47344765
def get_children(self):
47354766
yield from self.values
47364767

4768+
def _infer(
4769+
self, context: InferenceContext | None = None, **kwargs: Any
4770+
) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
4771+
yield from self._infer_from_values(self.values, context)
4772+
4773+
@classmethod
4774+
def _infer_from_values(
4775+
cls, nodes: list[NodeNG], context: InferenceContext | None = None, **kwargs: Any
4776+
) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
4777+
if len(nodes) == 1:
4778+
yield from nodes[0]._infer(context, **kwargs)
4779+
return
4780+
uninferable_already_generated = False
4781+
for prefix in nodes[0]._infer(context, **kwargs):
4782+
for suffix in cls._infer_from_values(nodes[1:], context, **kwargs):
4783+
result = ""
4784+
for node in (prefix, suffix):
4785+
if isinstance(node, Const):
4786+
result += str(node.value)
4787+
continue
4788+
result += MISSING_VALUE
4789+
if MISSING_VALUE in result:
4790+
if not uninferable_already_generated:
4791+
uninferable_already_generated = True
4792+
yield util.Uninferable
4793+
else:
4794+
yield Const(result)
4795+
47374796

47384797
class NamedExpr(_base_nodes.AssignTypeNode):
47394798
"""Represents the assignment from the assignment expression

tests/test_inference.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import pytest
2020

2121
from astroid import (
22+
Const,
2223
Slice,
2324
Uninferable,
2425
arguments,
@@ -652,6 +653,34 @@ def process_line(word_pos):
652653
)
653654
)
654655

656+
def test_fstring_inference(self) -> None:
657+
code = """
658+
name = "John"
659+
result = f"Hello {name}!"
660+
"""
661+
ast = parse(code, __name__)
662+
node = ast["result"]
663+
inferred = node.inferred()
664+
self.assertEqual(len(inferred), 1)
665+
value_node = inferred[0]
666+
self.assertIsInstance(value_node, Const)
667+
self.assertEqual(value_node.value, "Hello John!")
668+
669+
def test_formatted_fstring_inference(self) -> None:
670+
code = """
671+
width = 10
672+
precision = 4
673+
value = 12.34567
674+
result = f"result: {value:{width}.{precision}}!"
675+
"""
676+
ast = parse(code, __name__)
677+
node = ast["result"]
678+
inferred = node.inferred()
679+
self.assertEqual(len(inferred), 1)
680+
value_node = inferred[0]
681+
self.assertIsInstance(value_node, Const)
682+
self.assertEqual(value_node.value, "result: 12.35!")
683+
655684
def test_float_complex_ambiguity(self) -> None:
656685
code = '''
657686
def no_conjugate_member(magic_flag): #@

0 commit comments

Comments
 (0)