From c7bb7707a6d4f7e1fb8705e7a8e6052875cf1de9 Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Tue, 6 Aug 2024 10:40:26 +0200 Subject: [PATCH 1/9] catch exceptions when calling string.format --- astroid/nodes/node_classes.py | 30 ++++++++++++++------------ tests/test_inference.py | 40 +++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index c1c7af36da..206d2f4b2f 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -4687,19 +4687,23 @@ def _infer( uninferable_already_generated = True continue for value in self.value.infer(context, **kwargs): - if not isinstance(value, Const): - if not uninferable_already_generated: - yield util.Uninferable - uninferable_already_generated = True - continue - formatted = format(value.value, format_spec.value) - yield Const( - formatted, - lineno=self.lineno, - col_offset=self.col_offset, - end_lineno=self.end_lineno, - end_col_offset=self.end_col_offset, - ) + if isinstance(value, Const): + try: + formatted = format(value.value, format_spec.value) + yield Const( + formatted, + lineno=self.lineno, + col_offset=self.col_offset, + end_lineno=self.end_lineno, + end_col_offset=self.end_col_offset, + ) + continue + except ValueError: # happens when format_spec.value is invalid + pass # fall through + if not uninferable_already_generated: + yield util.Uninferable + uninferable_already_generated = True + continue MISSING_VALUE = "{MISSING_VALUE}" diff --git a/tests/test_inference.py b/tests/test_inference.py index 61378043c3..2fb1f5de58 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -666,20 +666,7 @@ def test_fstring_inference(self) -> None: self.assertIsInstance(value_node, Const) self.assertEqual(value_node.value, "Hello John!") - def test_formatted_fstring_inference(self) -> None: - code = """ - width = 10 - precision = 4 - value = 12.34567 - result = f"result: {value:{width}.{precision}}!" - """ - ast = parse(code, __name__) - node = ast["result"] - inferred = node.inferred() - self.assertEqual(len(inferred), 1) - value_node = inferred[0] - self.assertIsInstance(value_node, Const) - self.assertEqual(value_node.value, "result: 12.35!") + def test_float_complex_ambiguity(self) -> None: code = ''' @@ -5517,6 +5504,31 @@ class instance(object): self.assertIsInstance(inferred, Instance) +@pytest.mark.parametrize("code, result", [ + ("""width = 10 +precision = 4 +value = 12.34567 +result = f"result: {value:{width}.{precision}}!" +""", "result: 12.35!"), + ("""width = None +precision = 4 +value = 12.34567 +result = f"result: {value:{width}.{precision}}!" +""", None) +]) +def test_formatted_fstring_inference(code, result) -> None: + ast = parse(code, __name__) + node = ast["result"] + inferred = node.inferred() + assert len(inferred) == 1 + value_node = inferred[0] + if result is None: + assert value_node is util.Uninferable + else: + assert isinstance(value_node, Const) + assert value_node.value == result + + def test_augassign_recursion() -> None: """Make sure inference doesn't throw a RecursionError. From b4d3c8820541d58d69520d06dcfddba2647bf694 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 08:44:06 +0000 Subject: [PATCH 2/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_inference.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/tests/test_inference.py b/tests/test_inference.py index 2fb1f5de58..7bc50d6be6 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -666,8 +666,6 @@ def test_fstring_inference(self) -> None: self.assertIsInstance(value_node, Const) self.assertEqual(value_node.value, "Hello John!") - - def test_float_complex_ambiguity(self) -> None: code = ''' def no_conjugate_member(magic_flag): #@ @@ -5504,18 +5502,27 @@ class instance(object): self.assertIsInstance(inferred, Instance) -@pytest.mark.parametrize("code, result", [ - ("""width = 10 +@pytest.mark.parametrize( + "code, result", + [ + ( + """width = 10 precision = 4 value = 12.34567 result = f"result: {value:{width}.{precision}}!" -""", "result: 12.35!"), - ("""width = None +""", + "result: 12.35!", + ), + ( + """width = None precision = 4 value = 12.34567 result = f"result: {value:{width}.{precision}}!" -""", None) -]) +""", + None, + ), + ], +) def test_formatted_fstring_inference(code, result) -> None: ast = parse(code, __name__) node = ast["result"] From e8357255cb3dd84f3b42cb962e4e51852fd5059c Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Tue, 6 Aug 2024 10:50:10 +0200 Subject: [PATCH 3/9] more tests --- astroid/nodes/node_classes.py | 2 +- tests/test_inference.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index 206d2f4b2f..972cc6516b 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -4698,7 +4698,7 @@ def _infer( end_col_offset=self.end_col_offset, ) continue - except ValueError: # happens when format_spec.value is invalid + except (ValueError, TypeError): # happens when format_spec.value is invalid pass # fall through if not uninferable_already_generated: yield util.Uninferable diff --git a/tests/test_inference.py b/tests/test_inference.py index 2fb1f5de58..f748171d6d 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -5510,6 +5510,11 @@ class instance(object): value = 12.34567 result = f"result: {value:{width}.{precision}}!" """, "result: 12.35!"), +("""width = 10 +precision = 4 +value = None +result = f"result: {value:{width}.{precision}}!" +""", None), ("""width = None precision = 4 value = 12.34567 From 13de091ee6f5e51117078865f55ff1362055864d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 08:58:57 +0000 Subject: [PATCH 4/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- astroid/nodes/node_classes.py | 5 +- tests/test_inference.py | 118 +++++++++++++++++----------------- 2 files changed, 63 insertions(+), 60 deletions(-) diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index 972cc6516b..a674fd4d83 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -4698,7 +4698,10 @@ def _infer( end_col_offset=self.end_col_offset, ) continue - except (ValueError, TypeError): # happens when format_spec.value is invalid + except ( + ValueError, + TypeError, + ): # happens when format_spec.value is invalid pass # fall through if not uninferable_already_generated: yield util.Uninferable diff --git a/tests/test_inference.py b/tests/test_inference.py index ff147652ed..bc915190c6 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -76,10 +76,10 @@ def infer_default(self: Any, *args: InferenceContext) -> None: def _assertInferElts( - node_type: ABCMeta, - self: InferenceTest, - node: Any, - elts: list[int] | list[str], + node_type: ABCMeta, + self: InferenceTest, + node: Any, + elts: list[int] | list[str], ) -> None: inferred = next(node.infer()) self.assertIsInstance(inferred, node_type) @@ -102,7 +102,7 @@ def assertInferConst(self, node: nodes.Call, expected: str) -> None: self.assertEqual(inferred.value, expected) def assertInferDict( - self, node: nodes.Call | nodes.Dict | nodes.NodeNG, expected: Any + self, node: nodes.Call | nodes.Dict | nodes.NodeNG, expected: Any ) -> None: inferred = next(node.infer()) self.assertIsInstance(inferred, nodes.Dict) @@ -1030,7 +1030,7 @@ def test_do_import_module_performance(self) -> None: import_node.do_import_module() # calling file_from_module_name() indicates we didn't hit the cache with unittest.mock.patch.object( - manager.AstroidManager, "file_from_module_name", side_effect=AssertionError + manager.AstroidManager, "file_from_module_name", side_effect=AssertionError ): import_node.do_import_module() @@ -1042,13 +1042,13 @@ def _test_const_inferred(self, node: nodes.AssignName, value: float | str) -> No def test_unary_not(self) -> None: for code in ( - "a = not (1,); b = not ()", - "a = not {1:2}; b = not {}", - "a = not [1, 2]; b = not []", - "a = not {1, 2}; b = not set()", - "a = not 1; b = not 0", - 'a = not "a"; b = not ""', - 'a = not b"a"; b = not b""', + "a = not (1,); b = not ()", + "a = not {1:2}; b = not {}", + "a = not [1, 2]; b = not []", + "a = not {1, 2}; b = not set()", + "a = not 1; b = not 0", + 'a = not "a"; b = not ""', + 'a = not b"a"; b = not b""', ): ast = builder.string_build(code, __name__, __file__) self._test_const_inferred(ast["a"], False) @@ -3689,21 +3689,21 @@ class A(six.with_metaclass(Meta)): self.assertEqual(inferred.value, 42) def _slicing_test_helper( - self, - pairs: tuple[ - tuple[str, list[int] | str], - tuple[str, list[int] | str], - tuple[str, list[int] | str], - tuple[str, list[int] | str], - tuple[str, list[int] | str], - tuple[str, list[int] | str], - tuple[str, list[int] | str], - tuple[str, list[int] | str], - tuple[str, list[int] | str], - tuple[str, list[int] | str], - ], - cls: ABCMeta | type, - get_elts: Callable, + self, + pairs: tuple[ + tuple[str, list[int] | str], + tuple[str, list[int] | str], + tuple[str, list[int] | str], + tuple[str, list[int] | str], + tuple[str, list[int] | str], + tuple[str, list[int] | str], + tuple[str, list[int] | str], + tuple[str, list[int] | str], + tuple[str, list[int] | str], + tuple[str, list[int] | str], + ], + cls: ABCMeta | type, + get_elts: Callable, ) -> None: for code, expected in pairs: ast_node = extract_node(code) @@ -5064,7 +5064,7 @@ def test_type(self) -> None: class ArgumentsTest(unittest.TestCase): @staticmethod def _get_dict_value( - inferred: dict, + inferred: dict, ) -> list[tuple[str, int]] | list[tuple[str, str]]: items = inferred.items return sorted((key.value, value.value) for key, value in items) @@ -5392,7 +5392,7 @@ def _call_site_from_call(call: nodes.Call) -> CallSite: return arguments.CallSite.from_call(call) def _test_call_site_pair( - self, code: str, expected_args: list[int], expected_keywords: dict[str, int] + self, code: str, expected_args: list[int], expected_keywords: dict[str, int] ) -> None: ast_node = extract_node(code) call_site = self._call_site_from_call(ast_node) @@ -5406,7 +5406,7 @@ def _test_call_site_pair( self.assertEqual(call_site.keyword_arguments[keyword].value, value) def _test_call_site( - self, pairs: list[tuple[str, list[int], dict[str, int]]] + self, pairs: list[tuple[str, list[int], dict[str, int]]] ) -> None: for pair in pairs: self._test_call_site_pair(*pair) @@ -5507,30 +5507,30 @@ class instance(object): [ # regular f-string ( - """width = 10 + """width = 10 precision = 4 value = 12.34567 result = f"result: {value:{width}.{precision}}!" """, - "result: 12.35!", + "result: 12.35!", ), # unsupported format ( - """width = None + """width = None precision = 4 value = 12.34567 result = f"result: {value:{width}.{precision}}!" """, - None + None, ), # unsupported value ( - """width = 10 + """width = 10 precision = 4 value = None result = f"result: {value:{width}.{precision}}!" """, - None + None, ), ], ) @@ -6513,7 +6513,7 @@ def increment(self): "code,instance_name", [ ( - """ + """ class A: def __enter__(self): return self @@ -6524,10 +6524,10 @@ class B(A): with B() as b: b #@ """, - "B", + "B", ), ( - """ + """ class A: def __enter__(self): return A() @@ -6538,10 +6538,10 @@ class B(A): with B() as b: b #@ """, - "A", + "A", ), ( - """ + """ class A: def test(self): return A() @@ -6550,7 +6550,7 @@ def test(self): return A.test(self) B().test() """, - "A", + "A", ), ], ) @@ -6744,70 +6744,70 @@ def test_infer_dict_passes_context() -> None: "code,obj,obj_type", [ ( - """ + """ def klassmethod1(method): @classmethod def inner(cls): return method(cls) return inner - + class X(object): @klassmethod1 def x(cls): return 'X' X.x """, - BoundMethod, - "classmethod", + BoundMethod, + "classmethod", ), ( - """ + """ def staticmethod1(method): @staticmethod def inner(cls): return method(cls) return inner - + class X(object): @staticmethod1 def x(cls): return 'X' X.x """, - nodes.FunctionDef, - "staticmethod", + nodes.FunctionDef, + "staticmethod", ), ( - """ + """ def klassmethod1(method): def inner(cls): return method(cls) return classmethod(inner) - + class X(object): @klassmethod1 def x(cls): return 'X' X.x """, - BoundMethod, - "classmethod", + BoundMethod, + "classmethod", ), ( - """ + """ def staticmethod1(method): def inner(cls): return method(cls) return staticmethod(inner) - + class X(object): @staticmethod1 def x(cls): return 'X' X.x """, - nodes.FunctionDef, - "staticmethod", + nodes.FunctionDef, + "staticmethod", ), ], ) From 4a40de4584e3ddfba4a65ece31a2e5cc12209172 Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Tue, 6 Aug 2024 11:08:11 +0200 Subject: [PATCH 5/9] undo formatting --- tests/test_inference.py | 124 ++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/tests/test_inference.py b/tests/test_inference.py index bc915190c6..243f2ab781 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -6514,16 +6514,16 @@ def increment(self): [ ( """ - class A: - def __enter__(self): - return self - def __exit__(self, err_type, err, traceback): - return - class B(A): - pass - with B() as b: - b #@ - """, + class A: + def __enter__(self): + return self + def __exit__(self, err_type, err, traceback): + return + class B(A): + pass + with B() as b: + b #@ + """, "B", ), ( @@ -6542,14 +6542,14 @@ class B(A): ), ( """ - class A: - def test(self): - return A() - class B(A): - def test(self): - return A.test(self) - B().test() - """, + class A: + def test(self): + return A() + class B(A): + def test(self): + return A.test(self) + B().test() + """, "A", ), ], @@ -6745,67 +6745,67 @@ def test_infer_dict_passes_context() -> None: [ ( """ - def klassmethod1(method): - @classmethod - def inner(cls): - return method(cls) - return inner - - class X(object): - @klassmethod1 - def x(cls): - return 'X' - X.x - """, + def klassmethod1(method): + @classmethod + def inner(cls): + return method(cls) + return inner + + class X(object): + @klassmethod1 + def x(cls): + return 'X' + X.x + """, BoundMethod, "classmethod", ), ( """ - def staticmethod1(method): - @staticmethod - def inner(cls): - return method(cls) - return inner - - class X(object): - @staticmethod1 - def x(cls): - return 'X' - X.x - """, + def staticmethod1(method): + @staticmethod + def inner(cls): + return method(cls) + return inner + + class X(object): + @staticmethod1 + def x(cls): + return 'X' + X.x + """, nodes.FunctionDef, "staticmethod", ), ( """ - def klassmethod1(method): - def inner(cls): - return method(cls) - return classmethod(inner) + def klassmethod1(method): + def inner(cls): + return method(cls) + return classmethod(inner) - class X(object): - @klassmethod1 - def x(cls): - return 'X' - X.x - """, + class X(object): + @klassmethod1 + def x(cls): + return 'X' + X.x + """, BoundMethod, "classmethod", ), ( """ - def staticmethod1(method): - def inner(cls): - return method(cls) - return staticmethod(inner) + def staticmethod1(method): + def inner(cls): + return method(cls) + return staticmethod(inner) - class X(object): - @staticmethod1 - def x(cls): - return 'X' - X.x - """, + class X(object): + @staticmethod1 + def x(cls): + return 'X' + X.x + """, nodes.FunctionDef, "staticmethod", ), From 944edf986039e082fe8581e8d921ec7926ec423b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 09:08:29 +0000 Subject: [PATCH 6/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_inference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_inference.py b/tests/test_inference.py index 243f2ab781..de4f0522c4 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -6750,7 +6750,7 @@ def klassmethod1(method): def inner(cls): return method(cls) return inner - + class X(object): @klassmethod1 def x(cls): From 4eed6b0b43a3a1dfea5670d2af6b89c147e2bbb8 Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Tue, 6 Aug 2024 11:10:09 +0200 Subject: [PATCH 7/9] undo formatting --- tests/test_inference.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_inference.py b/tests/test_inference.py index 243f2ab781..a8b11b1614 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -6528,16 +6528,16 @@ class B(A): ), ( """ - class A: - def __enter__(self): - return A() - def __exit__(self, err_type, err, traceback): - return - class B(A): - pass - with B() as b: - b #@ - """, + class A: + def __enter__(self): + return A() + def __exit__(self, err_type, err, traceback): + return + class B(A): + pass + with B() as b: + b #@ + """, "A", ), ( @@ -6750,7 +6750,7 @@ def klassmethod1(method): def inner(cls): return method(cls) return inner - + class X(object): @klassmethod1 def x(cls): From 31dcc981c76fd2317666d9f4bacb33f78a81e31c Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 6 Aug 2024 07:47:18 -0400 Subject: [PATCH 8/9] cosmetic --- astroid/nodes/node_classes.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index a674fd4d83..1924c78eba 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -4698,10 +4698,8 @@ def _infer( end_col_offset=self.end_col_offset, ) continue - except ( - ValueError, - TypeError, - ): # happens when format_spec.value is invalid + except (ValueError, TypeError): + # happens when format_spec.value is invalid pass # fall through if not uninferable_already_generated: yield util.Uninferable From f02df6987b14a8329db3976839974fac76782cfa Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 6 Aug 2024 07:50:16 -0400 Subject: [PATCH 9/9] Add changelog --- ChangeLog | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog b/ChangeLog index 4560e5d2b7..c08b1cbf2c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -13,6 +13,9 @@ What's New in astroid 3.3.1? ============================ Release date: TBA +* Fix a crash introduced in 3.3.0 involving invalid format strings. + + Closes #2492 What's New in astroid 3.3.0?