8000 gh-121210: handle nodes with missing attributes/fields in `ast.compar… · python/cpython@15232a0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 15232a0

Browse files
authored
gh-121210: handle nodes with missing attributes/fields in ast.compare (#121211)
1 parent 7a807c3 commit 15232a0

File tree

3 files changed

+36
-4
lines changed

3 files changed

+36
-4
lines changed

Lib/ast.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,8 @@ def compare(
422422
might differ in whitespace or similar details.
423423
"""
424424

425+
sentinel = object() # handle the possibility of a missing attribute/field
426+
425427
def _compare(a, b):
426428
# Compare two fields on an AST object, which may themselves be
427429
# AST objects, lists of AST objects, or primitive ASDL types
@@ -449,8 +451,14 @@ def _compare_fields(a, b):
449451
if a._fields != b._fields:
450452
return False
451453
for field in a._fields:
452-
a_field = getattr(a, field)
453-
b_field = getattr(b, field)
454+
a_field = getattr(a, field, sentinel)
455+
b_field = getattr(b, field, sentinel)
456+
if a_field is sentinel and b_field is sentinel:
457+
# both nodes are missing a field at runtime
458+
continue
459+
if a_field is sentinel or b_field is sentinel:
460+
# one of the node is missing a field
461+
return False
454462
if not _compare(a_field, b_field):
455463
return False
456464
else:
@@ -461,8 +469,11 @@ def _compare_attributes(a, b):
461469
return False
462470
# Attributes are always ints.
463471
for attr in a._attributes:
464-
a_attr = getattr(a, attr)
465-
b_attr = getattr(b, attr)
472+
a_attr = getattr(a, attr, sentinel)
473+
b_attr = getattr(b, attr, sentinel)
474+
if a_attr is sentinel and b_attr is sentinel:
475+
# both nodes are missing an attribute at runtime
476+
continue
466477
if a_attr != b_attr:
467478
return False
468479
else:

Lib/test/test_ast.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,15 @@ def test_compare_fieldless(self):
948948
self.assertTrue(ast.compare(ast.Add(), ast.Add()))
949949
self.assertFalse(ast.compare(ast.Sub(), ast.Add()))
950950

951+
# test that missing runtime fields is handled in ast.compare()
952+
a1, a2 = ast.Name('a'), ast.Name('a')
953+
self.assertTrue(ast.compare(a1, a2))
954+
self.assertTrue(ast.compare(a1, a2))
955+
del a1.id
956+
self.assertFalse(ast.compare(a1, a2))
957+
del a2.id
958+
self.assertTrue(ast.compare(a1, a2))
959+
951960
def test_compare_modes(self):
952961
for mode, sources in (
953962
("exec", exec_tests),
@@ -970,6 +979,16 @@ def parse(a, b):
970979
self.assertTrue(ast.compare(a, b, compare_attributes=False))
971980
self.assertFalse(ast.compare(a, b, compare_attributes=True))
972981

982+
def test_compare_attributes_option_missing_attribute(self):
983+
# test that missing runtime attributes is handled in ast.compare()
984+
a1, a2 = ast.Name('a', lineno=1), ast.Name('a', lineno=1)
985+
self.assertTrue(ast.compare(a1, a2))
986+
self.assertTrue(ast.compare(a1, a2, compare_attributes=True))
987+
del a1.lineno
988+
self.assertFalse(ast.compare(a1, a2, compare_attributes=True))
989+
del a2.lineno
990+
self.assertTrue(ast.compare(a1, a2, compare_attributes=True))
991+
973992
def test_positional_only_feature_version(self):
974993
ast.parse('def foo(x, /): ...', feature_version=(3, 8))
975994
ast.parse('def bar(x=1, /): ...', feature_version=(3, 8))
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Handle AST nodes with missing runtime fields or attributes in
2+
:func:`ast.compare`. Patch by Bénédikt Tran.

0 commit comments

Comments
 (0)
0