From 15232a0819a2f7e0f448f28f2e6081912d10e7cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:53:17 +0200 Subject: [PATCH] gh-121210: handle nodes with missing attributes/fields in `ast.compare` (#121211) --- Lib/ast.py | 19 +++++++++++++++---- Lib/test/test_ast.py | 19 +++++++++++++++++++ ...-07-01-11-23-18.gh-issue-121210.cD0zfn.rst | 2 ++ 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-07-01-11-23-18.gh-issue-121210.cD0zfn.rst diff --git a/Lib/ast.py b/Lib/ast.py index fb4d21b87d8bd0..a954d4a97d3c22 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -422,6 +422,8 @@ def compare( might differ in whitespace or similar details. """ + sentinel = object() # handle the possibility of a missing attribute/field + def _compare(a, b): # Compare two fields on an AST object, which may themselves be # AST objects, lists of AST objects, or primitive ASDL types @@ -449,8 +451,14 @@ def _compare_fields(a, b): if a._fields != b._fields: return False for field in a._fields: - a_field = getattr(a, field) - b_field = getattr(b, field) + a_field = getattr(a, field, sentinel) + b_field = getattr(b, field, sentinel) + if a_field is sentinel and b_field is sentinel: + # both nodes are missing a field at runtime + continue + if a_field is sentinel or b_field is sentinel: + # one of the node is missing a field + return False if not _compare(a_field, b_field): return False else: @@ -461,8 +469,11 @@ def _compare_attributes(a, b): return False # Attributes are always ints. for attr in a._attributes: - a_attr = getattr(a, attr) - b_attr = getattr(b, attr) + a_attr = getattr(a, attr, sentinel) + b_attr = getattr(b, attr, sentinel) + if a_attr is sentinel and b_attr is sentinel: + # both nodes are missing an attribute at runtime + continue if a_attr != b_attr: return False else: diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index 5d71d524516df2..fbd19620311159 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -948,6 +948,15 @@ def test_compare_fieldless(self): self.assertTrue(ast.compare(ast.Add(), ast.Add())) self.assertFalse(ast.compare(ast.Sub(), ast.Add())) + # test that missing runtime fields is handled in ast.compare() + a1, a2 = ast.Name('a'), ast.Name('a') + self.assertTrue(ast.compare(a1, a2)) + self.assertTrue(ast.compare(a1, a2)) + del a1.id + self.assertFalse(ast.compare(a1, a2)) + del a2.id + self.assertTrue(ast.compare(a1, a2)) + def test_compare_modes(self): for mode, sources in ( ("exec", exec_tests), @@ -970,6 +979,16 @@ def parse(a, b): self.assertTrue(ast.compare(a, b, compare_attributes=False)) self.assertFalse(ast.compare(a, b, compare_attributes=True)) + def test_compare_attributes_option_missing_attribute(self): + # test that missing runtime attributes is handled in ast.compare() + a1, a2 = ast.Name('a', lineno=1), ast.Name('a', lineno=1) + self.assertTrue(ast.compare(a1, a2)) + self.assertTrue(ast.compare(a1, a2, compare_attributes=True)) + del a1.lineno + self.assertFalse(ast.compare(a1, a2, compare_attributes=True)) + del a2.lineno + self.assertTrue(ast.compare(a1, a2, compare_attributes=True)) + def test_positional_only_feature_version(self): ast.parse('def foo(x, /): ...', feature_version=(3, 8)) ast.parse('def bar(x=1, /): ...', feature_version=(3, 8)) diff --git a/Misc/NEWS.d/next/Library/2024-07-01-11-23-18.gh-issue-121210.cD0zfn.rst b/Misc/NEWS.d/next/Library/2024-07-01-11-23-18.gh-issue-121210.cD0zfn.rst new file mode 100644 index 00000000000000..55d5b221bf0765 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-07-01-11-23-18.gh-issue-121210.cD0zfn.rst @@ -0,0 +1,2 @@ +Handle AST nodes with missing runtime fields or attributes in +:func:`ast.compare`. Patch by Bénédikt Tran.