From cfb508ff34d0aef37ca0013fd9b77b8d423a119c Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Sat, 28 Mar 2020 21:05:33 +0300 Subject: [PATCH 01/29] bpo-15987: Implement ast.compare --- Doc/library/ast.rst | 11 ++ Doc/whatsnew/3.9.rst | 3 + Lib/ast.py | 63 ++++++++++ Lib/test/test_ast.py | 116 +++++++++++++++++- .../2020-03-28-21-00-54.bpo-15987.aBL8XS.rst | 2 + 5 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index fc04114949c0c3..583a023c643d75 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -1748,6 +1748,17 @@ and classes for traversing abstract syntax trees: Added the *indent* option. +.. function:: compare(a, b, /, *, compare_types=True, compare_fields=True, compare_attributes=False) + + Recursively compare given 2 AST nodes. If *compare_types* is ``False``, the + field values won't be checked whether they belong to same type or not. If + *compare_fields* is ``True``, members of ``_fields`` attribute on both node's + type will be checked. If *compare_attributes* is ``True``, members of + ``_attributes`` attribute on both node's will be compared. + + .. versionadded:: 3.9 + + .. _ast-cli: Command-Line Usage diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index 778e443f8d0777..68088ce4e4c5f3 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -167,6 +167,9 @@ that would produce an equivalent :class:`ast.AST` object when parsed. Added docstrings to AST nodes that contains the ASDL signature used to construct that node. (Contributed by Batuhan Taskaya in :issue:`39638`.) +Added :func:`ast.compare` for comparing 2 AST node. +(Contributed by Batuhan Taskaya in :issue:`15987`) + asyncio ------- diff --git a/Lib/ast.py b/Lib/ast.py index f51c71fb8c608c..186a8f47f9ec2f 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -368,6 +368,69 @@ def walk(node): yield node +def compare( + a, + b, + /, + *, + compare_types=True, + compare_fields=True, + compare_attributes=False, +): + """ + Compares recursively given two ast nodes. If *compare_types* is False, the + field values won't be checked whether they belong to same type or not. If + *compare_fields* is True, members of `_fields` attribute on both node's type + will be checked. If *compare_attributes* is True, members of `_attributes` + attribute on both node's will be compared. + """ + + def _compare(a, b): + if compare_types and type(a) is not type(b): + return False + elif isinstance(a, AST): + return compare( + a, + b, + compare_attributes=compare_attributes, + compare_fields=compare_fields, + compare_types=compare_types, + ) + elif isinstance(a, list): + if len(a) != len(b): + return False + for a_item, b_item in zip(a, b): + if not _compare(a_item, b_item): + return False + else: + return True + else: + return a == b + + def _compare_member(member): + for field in getattr(a, member): + if not hasattr(a, field) and not hasattr(b, field): + continue + if not (hasattr(a, field) and hasattr(b, field)): + return False + a_field = getattr(a, field) + b_field = getattr(b, field) + if not _compare(a_field, b_field): + return False + else: + return True + + if type(a) is not type(b): + return False + if compare_fields: + if not _compare_member("_fields"): + return False + if compare_attributes: + if not _compare_member("_attributes"): + return False + return True + + class NodeVisitor(object): """ A node visitor base class that walks the abstract syntax tree and calls a diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index 3fd982c79ea21e..cc624a5a529778 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -1,7 +1,9 @@ import ast import dis import os +import random import sys +import tokenize import unittest import warnings import weakref @@ -25,6 +27,9 @@ def to_tuple(t): result.append(to_tuple(getattr(t, f))) return tuple(result) +STDLIB = os.path.dirname(ast.__file__) +STDLIB_FILES = [fn for fn in os.listdir(STDLIB) if fn.endswith(".py")] +STDLIB_FILES.extend(["test/test_grammar.py", "test/test_unpack_ex.py"]) # These tests are compiled through "exec" # There should be at least one test per statement @@ -654,6 +659,110 @@ def test_ast_asdl_signature(self): expressions[0] = f"expr = {ast.expr.__subclasses__()[0].__doc__}" self.assertCountEqual(ast.expr.__doc__.split("\n"), expressions) + self.assertTrue(ast.compare(ast.parse("x = 10"), ast.parse("x = 10"))) + self.assertFalse(ast.compare(ast.parse("x = 10"), ast.parse(""))) + self.assertFalse(ast.compare(ast.parse("x = 10"), ast.parse("x"))) + self.assertFalse( + ast.compare(ast.parse("x = 10;y = 20"), ast.parse("class C:pass")) + ) + + def test_compare_literals(self): + constants = ( + -20, + 20, + 20.0, + 1, + 1.0, + True, + 0, + False, + frozenset(), + tuple(), + "ABCD", + "abcd", + "中文字", + 1e1000, + -1e1000, + ) + for next_index, constant in enumerate(constants[:-1], 1): + next_constant = constants[next_index] + with self.subTest(literal=constant, next_literal=next_constant): + self.assertTrue( + ast.compare(ast.Constant(constant), ast.Constant(constant)) + ) + self.assertFalse( + ast.compare( + ast.Constant(constant), ast.Constant(next_constant) + ) + ) + + same_looking_literal_cases = [ + {1, 1.0, True, 1 + 0j}, + {0, 0.0, False, 0 + 0j}, + ] + for same_looking_literals in same_looking_literal_cases: + for literal in same_looking_literals: + for same_looking_literal in same_looking_literals - {literal}: + self.assertFalse( + ast.compare( + ast.Constant(literal), + ast.Constant(same_looking_literal), + ) + ) + + def test_compare_fieldless(self): + self.assertTrue(ast.compare(ast.Add(), ast.Add())) + self.assertFalse(ast.compare(ast.Sub(), ast.Add())) + self.assertFalse(ast.compare(ast.Sub(), ast.Constant())) + + def test_compare_stdlib(self): + if support.is_resource_enabled("cpu"): + files = STDLIB_FILES + else: + files = random.sample(STDLIB_FILES, 10) + + for module in files: + with self.subTest(module): + fn = os.path.join(STDLIB, module) + with tokenize.open(fn) as fp: + source = fp.read() + a = ast.parse(source, fn) + b = ast.parse(source, fn) + self.assertTrue( + ast.compare(a, b), f"{ast.dump(a)} != {ast.dump(b)}" + ) + + def test_compare_tests(self): + for mode, sources in ( + ("exec", exec_tests), + ("eval", eval_tests), + ("single", single_tests), + ): + for source in sources: + a = ast.parse(source, mode=mode) + b = ast.parse(source, mode=mode) + self.assertTrue( + ast.compare(a, b), f"{ast.dump(a)} != {ast.dump(b)}" + ) + + def test_compare_options(self): + def parse(a, b): + return ast.parse(a), ast.parse(b) + + a, b = parse("2 + 2", "2+2") + self.assertTrue(ast.compare(a, b, compare_attributes=False)) + self.assertFalse(ast.compare(a, b, compare_attributes=True)) + + a, b = parse("1", "1.0") + self.assertTrue(ast.compare(a, b, compare_types=False)) + self.assertFalse(ast.compare(a, b, compare_types=True)) + + a, b = parse("1", "2") + self.assertTrue(ast.compare(a, b, compare_fields=False, compare_attributes=False)) + self.assertTrue(ast.compare(a, b, compare_fields=False, compare_attributes=True)) + self.assertFalse(ast.compare(a, b, compare_fields=True, compare_attributes=False)) + self.assertFalse(ast.compare(a, b, compare_fields=True, compare_attributes=True)) + class ASTHelpers_Test(unittest.TestCase): maxDiff = None @@ -1369,12 +1478,9 @@ def test_nameconstant(self): self.expr(ast.NameConstant(4)) def test_stdlib_validates(self): - stdlib = os.path.dirname(ast.__file__) - tests = [fn for fn in os.listdir(stdlib) if fn.endswith(".py")] - tests.extend(["test/test_grammar.py", "test/test_unpack_ex.py"]) - for module in tests: + for module in STDLIB_FILES: with self.subTest(module): - fn = os.path.join(stdlib, module) + fn = os.path.join(STDLIB, module) with open(fn, "r", encoding="utf-8") as fp: source = fp.read() mod = ast.parse(source, fn) diff --git a/Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst b/Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst new file mode 100644 index 00000000000000..2bd43d2aea316c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst @@ -0,0 +1,2 @@ +Implemented :func:`ast.compare` for comparing 2 AST node. Patch by Batuhan +Taskaya. From 0441023c81912ee28e60fc83a5bd41badf6efce3 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Tue, 31 Mar 2020 23:03:16 +0300 Subject: [PATCH 02/29] unwrap ifs a level --- Lib/ast.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index 186a8f47f9ec2f..809b5adfd82e85 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -422,12 +422,10 @@ def _compare_member(member): if type(a) is not type(b): return False - if compare_fields: - if not _compare_member("_fields"): - return False - if compare_attributes: - if not _compare_member("_attributes"): - return False + if compare_fields and not _compare_member("_fields"): + return False + if compare_attributes and not _compare_member("_attributes"): + return False return True From 52f428ed1d62ba0dea3fddf9404f424fd94cc560 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton Date: Mon, 20 May 2024 16:41:00 -0400 Subject: [PATCH 03/29] A few revision to clarify some subtleties of comparing AST objects. Use separate helper functions to compare _fields and _attributes, because _attributes are always simple strings. Move the type comparison test to the point where it is relevant, rather than making unnecessary type tests. Add comments to explain the logic in more detail. --- Lib/ast.py | 38 +++++++++++++++++++++++++++----------- Lib/test/test_ast.py | 1 + 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index 9c9a130cd766df..63af0e79359d69 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -419,9 +419,10 @@ def compare( """ def _compare(a, b): - if compare_types and type(a) is not type(b): - return False - elif isinstance(a, AST): + # Compare two fields on an AST object, which may themselves be + # AST objects, lists of AST objects, or primitive ASDL types + # like identifiers and constants. + if isinstance(a, AST): return compare( a, b, @@ -430,6 +431,8 @@ def _compare(a, b): compare_types=compare_types, ) elif isinstance(a, list): + # If a field is repeated, then both objects will represent + # the value as a list. if len(a) != len(b): return False for a_item, b_item in zip(a, b): @@ -438,14 +441,15 @@ def _compare(a, b): else: return True else: + # The only case where the type comparison matters is + # Constant() notes that could have different objects with + # the same value, e.g. Constant(1) and Constant(1.0). + if compare_types and type(a) is not type(b): + return False return a == b - def _compare_member(member): - for field in getattr(a, member): - if not hasattr(a, field) and not hasattr(b, field): - continue - if not (hasattr(a, field) and hasattr(b, field)): - return False + def _compare_fields(): + for field in a._fields: a_field = getattr(a, field) b_field = getattr(b, field) if not _compare(a_field, b_field): @@ -453,11 +457,23 @@ def _compare_member(member): else: return True + def _compare_attributes(): + # Attributes are always strings. + for attr in a._attributes: + a_attr = getattr(a, attr) + b_attr = getattr(b, attr) + if a_attr != b_attr: + return False + else: + return True + if type(a) is not type(b): return False - if compare_fields and not _compare_member("_fields"): + # a and b are guaranteed to have the same type, so they must also + # have identical values for _fields and _attributes. + if compare_fields and not _compare_fields(): return False - if compare_attributes and not _compare_member("_attributes"): + if compare_attributes and not _compare_attributes(): return False return True diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index 7fa56a2a5e417c..ff0e99704d9bfe 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -1071,6 +1071,7 @@ def test_ast_asdl_signature(self): expressions[0] = f"expr = {ast.expr.__subclasses__()[0].__doc__}" self.assertCountEqual(ast.expr.__doc__.split("\n"), expressions) + def test_compare_basics(self): self.assertTrue(ast.compare(ast.parse("x = 10"), ast.parse("x = 10"))) self.assertFalse(ast.compare(ast.parse("x = 10"), ast.parse(""))) self.assertFalse(ast.compare(ast.parse("x = 10"), ast.parse("x"))) From 5f37b9117950a8cc7c16273b304f53b821b97fbf Mon Sep 17 00:00:00 2001 From: Jeremy Hylton Date: Mon, 20 May 2024 17:19:18 -0400 Subject: [PATCH 04/29] Remove the compare_fields option. The basic functionality here is to compare asts recursively. If compare_fields is False, then the comparison is not recursive. It just compares that the top-level AST objects are the same and have the same attributes. This option doesn't seem interesting enough to offer as a feature. Revise the docstring for compare() to explain the options in more detail. --- Lib/ast.py | 25 ++++++++++++++++--------- Lib/test/test_ast.py | 6 ------ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index 63af0e79359d69..476a03ba81ce19 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -407,15 +407,23 @@ def compare( /, *, compare_types=True, - compare_fields=True, compare_attributes=False, ): - """ - Compares recursively given two ast nodes. If *compare_types* is False, the - field values won't be checked whether they belong to same type or not. If - *compare_fields* is True, members of `_fields` attribute on both node's type - will be checked. If *compare_attributes* is True, members of `_attributes` - attribute on both node's will be compared. + """Compares recursively given two ast nodes. + + There are two options that control how the comparison is done. + + compare_types affects how Constant values are compared. If + compare_types is True (default), then Constant values must have + the same type and value to be equal. If compare_type is False, + then Constant values will compare equal if the type Python objects + are equal regardless of type, e.g. 1.0 == 1. + + compare_attributes affects whether AST attributes are considered + in the comparison. If compare_attributes is False (default), then + attributes are ignored. Otherwise they must all be equal. This + option is useful to look for asts that are structurally equal but + might differ in whitespace or similar details. """ def _compare(a, b): @@ -427,7 +435,6 @@ def _compare(a, b): a, b, compare_attributes=compare_attributes, - compare_fields=compare_fields, compare_types=compare_types, ) elif isinstance(a, list): @@ -471,7 +478,7 @@ def _compare_attributes(): return False # a and b are guaranteed to have the same type, so they must also # have identical values for _fields and _attributes. - if compare_fields and not _compare_fields(): + if not _compare_fields(): return False if compare_attributes and not _compare_attributes(): return False diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index ff0e99704d9bfe..1741decc639743 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -1170,12 +1170,6 @@ def parse(a, b): self.assertTrue(ast.compare(a, b, compare_types=False)) self.assertFalse(ast.compare(a, b, compare_types=True)) - a, b = parse("1", "2") - self.assertTrue(ast.compare(a, b, compare_fields=False, compare_attributes=False)) - self.assertTrue(ast.compare(a, b, compare_fields=False, compare_attributes=True)) - self.assertFalse(ast.compare(a, b, compare_fields=True, compare_attributes=False)) - self.assertFalse(ast.compare(a, b, compare_fields=True, 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)) From c0bb0e91efd624cd69284ee2f6fe4231ff5009c9 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton Date: Mon, 20 May 2024 17:39:26 -0400 Subject: [PATCH 05/29] Revise docstring and documentation for consistency. --- Doc/library/ast.rst | 22 +++++++++++++++------- Lib/ast.py | 10 +++++----- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index a3e54fa0ef6b63..393cac025893a0 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -2472,15 +2472,23 @@ effects on the compilation of a program: .. versionadded:: 3.8 -.. function:: compare(a, b, /, *, compare_types=True, compare_fields=True, compare_attributes=False) +.. function:: compare(a, b, /, *, compare_types=True, compare_attributes=False) - Recursively compare given 2 AST nodes. If *compare_types* is ``False``, the - field values won't be checked whether they belong to same type or not. If - *compare_fields* is ``True``, members of ``_fields`` attribute on both node's - type will be checked. If *compare_attributes* is ``True``, members of - ``_attributes`` attribute on both node's will be compared. + Recursively compares two ast nodes. - .. versionadded:: 3.9 + There are two options that control how the comparison is done. If + *compare_types* is ``True`` (default), then Constant objects must + have the same type and value to be equal. If *compare_types* is + ``False``, then Constant objects must only have the same values, + e.g. Constant(1.0) equals Constant(1). + + + *compare_attributes* affects whether AST attributes are considered + in the comparison. If compare_attributes is ``False`` (default), then + attributes are ignored. Otherwise they must all be equal. This + option is useful to look for asts that are structurally equal but + might differ in whitespace or similar details. + .. versionadded:: 3.14 .. _ast-cli: diff --git a/Lib/ast.py b/Lib/ast.py index 476a03ba81ce19..722c34dfe9ec17 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -409,15 +409,15 @@ def compare( compare_types=True, compare_attributes=False, ): - """Compares recursively given two ast nodes. + """Recursively compares two ast nodes. There are two options that control how the comparison is done. - compare_types affects how Constant values are compared. If - compare_types is True (default), then Constant values must have + compare_types affects how Constant objects are compared. If + compare_types is True (default), then Constant objects must have the same type and value to be equal. If compare_type is False, - then Constant values will compare equal if the type Python objects - are equal regardless of type, e.g. 1.0 == 1. + then Constant objects must only have the same value, + e.g. Constant(1.0) equals Constant(1). compare_attributes affects whether AST attributes are considered in the comparison. If compare_attributes is False (default), then From 13fbbc990596a26035d9b6b2d4d8b1b79996bc39 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton Date: Mon, 20 May 2024 17:40:13 -0400 Subject: [PATCH 06/29] This PR now describes a feature of 3.14. Revise what's new / news docs. --- Doc/whatsnew/3.14.rst | 7 +++++++ Doc/whatsnew/3.9.rst | 4 +--- .../next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst | 5 +++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 27c985bec104fe..4a9cd2e746f160 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -86,6 +86,13 @@ New Modules Improved Modules ================ +ast +--- + +Added :func:`ast.compare` for comparing 2 AST node. +(Contributed by Batuhan Taskaya in :issue:`15987`) + + Optimizations ============= diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index ece95b506c144b..5369c5b2c43e89 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -1,3 +1,4 @@ + **************************** What's New In Python 3.9 **************************** @@ -336,9 +337,6 @@ that would produce an equivalent :class:`ast.AST` object when parsed. Added docstrings to AST nodes that contains the ASDL signature used to construct that node. (Contributed by Batuhan Taskaya in :issue:`39638`.) -Added :func:`ast.compare` for comparing 2 AST node. -(Contributed by Batuhan Taskaya in :issue:`15987`) - asyncio ------- diff --git a/Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst b/Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst index 2bd43d2aea316c..bde8189d4bfac7 100644 --- a/Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst +++ b/Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst @@ -1,2 +1,3 @@ -Implemented :func:`ast.compare` for comparing 2 AST node. Patch by Batuhan -Taskaya. +Implemented :func:`ast.compare` for comparing two AST nodes. Patch by Batuhan +Taskaya with some help from Jeremy Hylton. + From f8f074734d3a49ed2fe4bb9b7472f3b3fbbaf29b Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Mon, 20 May 2024 18:06:50 -0400 Subject: [PATCH 07/29] Update 3.9.rst --- Doc/whatsnew/3.9.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index 5369c5b2c43e89..e29d37ca120b76 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -1,4 +1,3 @@ - **************************** What's New In Python 3.9 **************************** From deca2dab2648679f413b4b7da8e3c7c2d1d97fae Mon Sep 17 00:00:00 2001 From: Jeremy Hylton Date: Tue, 21 May 2024 11:58:48 -0400 Subject: [PATCH 08/29] Remove the compare_types option, which seems unnecessary. In practice the primary effect of this change was for Constant() nodes where the value of two constants were equal without being the same literal type, e.g. ast.compare(Constant(1), Constant(True), compare_types=True) was True. This behavior doesn't seem like it has a very strong motivation, and adds some complexity. We can add it back later if there's a clear need for it. --- Lib/ast.py | 18 ++---------------- Lib/test/test_ast.py | 5 ----- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index 722c34dfe9ec17..57820906675546 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -406,19 +406,10 @@ def compare( b, /, *, - compare_types=True, compare_attributes=False, ): """Recursively compares two ast nodes. - There are two options that control how the comparison is done. - - compare_types affects how Constant objects are compared. If - compare_types is True (default), then Constant objects must have - the same type and value to be equal. If compare_type is False, - then Constant objects must only have the same value, - e.g. Constant(1.0) equals Constant(1). - compare_attributes affects whether AST attributes are considered in the comparison. If compare_attributes is False (default), then attributes are ignored. Otherwise they must all be equal. This @@ -435,7 +426,7 @@ def _compare(a, b): a, b, compare_attributes=compare_attributes, - compare_types=compare_types, + ) elif isinstance(a, list): # If a field is repeated, then both objects will represent @@ -448,12 +439,7 @@ def _compare(a, b): else: return True else: - # The only case where the type comparison matters is - # Constant() notes that could have different objects with - # the same value, e.g. Constant(1) and Constant(1.0). - if compare_types and type(a) is not type(b): - return False - return a == b + return type(a) is type(b) and a == b def _compare_fields(): for field in a._fields: diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index 1741decc639743..99c4f67822eabf 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -1126,7 +1126,6 @@ def test_compare_literals(self): def test_compare_fieldless(self): self.assertTrue(ast.compare(ast.Add(), ast.Add())) self.assertFalse(ast.compare(ast.Sub(), ast.Add())) - self.assertFalse(ast.compare(ast.Sub(), ast.Constant())) def test_compare_stdlib(self): if support.is_resource_enabled("cpu"): @@ -1166,10 +1165,6 @@ def parse(a, b): self.assertTrue(ast.compare(a, b, compare_attributes=False)) self.assertFalse(ast.compare(a, b, compare_attributes=True)) - a, b = parse("1", "1.0") - self.assertTrue(ast.compare(a, b, compare_types=False)) - self.assertFalse(ast.compare(a, b, compare_types=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)) From 778874442ce045b0bdb83fa93b372ed7d3ec51b4 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Tue, 21 May 2024 12:18:00 -0400 Subject: [PATCH 09/29] Update Doc/whatsnew/3.14.rst Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> --- Doc/whatsnew/3.14.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 4a9cd2e746f160..358e181551c557 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -90,7 +90,7 @@ ast --- Added :func:`ast.compare` for comparing 2 AST node. -(Contributed by Batuhan Taskaya in :issue:`15987`) +(Contributed by Batuhan Taskaya and Jeremy Hylton in :issue:`15987`) From 05885de40bfec1bf85cddb4b0c1923b57753a95c Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Tue, 21 May 2024 12:18:52 -0400 Subject: [PATCH 10/29] Update Doc/whatsnew/3.14.rst Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> --- Doc/whatsnew/3.14.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 358e181551c557..535d59dad8f727 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -89,7 +89,7 @@ Improved Modules ast --- -Added :func:`ast.compare` for comparing 2 AST node. +Added :func:`ast.compare` for comparing 2 ASTs. (Contributed by Batuhan Taskaya and Jeremy Hylton in :issue:`15987`) From 3b7192ea16cc88ea0faee16028f5f4f3b8884b3f Mon Sep 17 00:00:00 2001 From: Jeremy Hylton Date: Tue, 21 May 2024 12:19:34 -0400 Subject: [PATCH 11/29] Remove compare_types from doc. Change ast node to AST. --- Doc/library/ast.rst | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index 393cac025893a0..deaef94243414a 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -2472,22 +2472,16 @@ effects on the compilation of a program: .. versionadded:: 3.8 -.. function:: compare(a, b, /, *, compare_types=True, compare_attributes=False) - - Recursively compares two ast nodes. - - There are two options that control how the comparison is done. If - *compare_types* is ``True`` (default), then Constant objects must - have the same type and value to be equal. If *compare_types* is - ``False``, then Constant objects must only have the same values, - e.g. Constant(1.0) equals Constant(1). +.. function:: compare(a, b, /, *, compare_attributes=False) + Recursively compares two ASTs. *compare_attributes* affects whether AST attributes are considered in the comparison. If compare_attributes is ``False`` (default), then attributes are ignored. Otherwise they must all be equal. This option is useful to look for asts that are structurally equal but might differ in whitespace or similar details. + .. versionadded:: 3.14 From b9b6c45c470ae332714f9d977337ddd9f8806488 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton Date: Tue, 21 May 2024 12:20:19 -0400 Subject: [PATCH 12/29] Change AST node to AST. --- .../next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst b/Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst index bde8189d4bfac7..675f5dae0918f8 100644 --- a/Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst +++ b/Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst @@ -1,3 +1,3 @@ -Implemented :func:`ast.compare` for comparing two AST nodes. Patch by Batuhan +Implemented :func:`ast.compare` for comparing two ASTs. Patch by Batuhan Taskaya with some help from Jeremy Hylton. From 0c72337c0f0187e4d55b6f382db9b8f176619314 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton Date: Tue, 21 May 2024 13:43:09 -0400 Subject: [PATCH 13/29] One more change of "ast nodes" to "ASTs" --- Lib/ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ast.py b/Lib/ast.py index 57820906675546..af40cc1b84539c 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -408,7 +408,7 @@ def compare( *, compare_attributes=False, ): - """Recursively compares two ast nodes. + """Recursively compares two ASTs. compare_attributes affects whether AST attributes are considered in the comparison. If compare_attributes is False (default), then From 6ad21c02925663d0ae6d399a653435d53a904d37 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton Date: Tue, 21 May 2024 13:48:23 -0400 Subject: [PATCH 14/29] Merge AST comparison into the test for AST validation. The chief benefit is that we only have to parse a large set of modules once. We can test parse and compare at the same time. --- Lib/test/test_ast.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index 99c4f67822eabf..d3c78f0c5d8983 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -7,7 +7,6 @@ import re import sys import textwrap -import tokenize import types import unittest import warnings @@ -1127,23 +1126,6 @@ def test_compare_fieldless(self): self.assertTrue(ast.compare(ast.Add(), ast.Add())) self.assertFalse(ast.compare(ast.Sub(), ast.Add())) - def test_compare_stdlib(self): - if support.is_resource_enabled("cpu"): - files = STDLIB_FILES - else: - files = random.sample(STDLIB_FILES, 10) - - for module in files: - with self.subTest(module): - fn = os.path.join(STDLIB, module) - with tokenize.open(fn) as fp: - source = fp.read() - a = ast.parse(source, fn) - b = ast.parse(source, fn) - self.assertTrue( - ast.compare(a, b), f"{ast.dump(a)} != {ast.dump(b)}" - ) - def test_compare_tests(self): for mode, sources in ( ("exec", exec_tests), @@ -2298,6 +2280,9 @@ def test_stdlib_validates(self): source = fp.read() mod = ast.parse(source, fn) compile(mod, fn, "exec") + mod2 = ast.parse(source, fn) + self.assertTrue(ast.compare(mod, mod2)) + self.assertTrue(False) constant_1 = ast.Constant(1) pattern_1 = ast.MatchValue(constant_1) From ec8af39a4429de323f238a67001fc7cea336bb00 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton Date: Tue, 21 May 2024 13:50:04 -0400 Subject: [PATCH 15/29] Merge tests for AST parsing and comparison. This change means that we only need to parse the full set of modules from the standard library once. --- Lib/test/test_ast.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index d3c78f0c5d8983..f91075533bde5b 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -3,7 +3,6 @@ import dis import enum import os -import random import re import sys import textwrap @@ -2282,7 +2281,6 @@ def test_stdlib_validates(self): compile(mod, fn, "exec") mod2 = ast.parse(source, fn) self.assertTrue(ast.compare(mod, mod2)) - self.assertTrue(False) constant_1 = ast.Constant(1) pattern_1 = ast.MatchValue(constant_1) From 75ba0028248347b581082f0be72a9e7aaffd778f Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Tue, 21 May 2024 14:12:03 -0400 Subject: [PATCH 16/29] Improve description of compare_attributes arg Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> --- Doc/library/ast.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index deaef94243414a..88c0e6799ff4f5 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -2479,7 +2479,7 @@ effects on the compilation of a program: *compare_attributes* affects whether AST attributes are considered in the comparison. If compare_attributes is ``False`` (default), then attributes are ignored. Otherwise they must all be equal. This - option is useful to look for asts that are structurally equal but + option is useful to check whether the ASTs are structurally equal but might differ in whitespace or similar details. .. versionadded:: 3.14 From 5b01405531b32367d5923131ecab188cb5a96727 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Tue, 21 May 2024 14:12:56 -0400 Subject: [PATCH 17/29] AP style: Spell out numbers under 10 Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> --- Doc/whatsnew/3.14.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 535d59dad8f727..39172ac60cf1e0 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -89,7 +89,7 @@ Improved Modules ast --- -Added :func:`ast.compare` for comparing 2 ASTs. +Added :func:`ast.compare` for comparing two ASTs. (Contributed by Batuhan Taskaya and Jeremy Hylton in :issue:`15987`) From 69be6d227cef5ce3d7e383efac0850f7ac5c2957 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Tue, 21 May 2024 14:14:10 -0400 Subject: [PATCH 18/29] Improve description of compare_attributes arg Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> --- Lib/ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ast.py b/Lib/ast.py index af40cc1b84539c..ced9dfce11fec3 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -413,7 +413,7 @@ def compare( compare_attributes affects whether AST attributes are considered in the comparison. If compare_attributes is False (default), then attributes are ignored. Otherwise they must all be equal. This - option is useful to look for asts that are structurally equal but + option is useful to check whether the ASTs are structurally equal but might differ in whitespace or similar details. """ From bdd2d66594bf7c82365fa243e13ca677c8aa57a5 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton Date: Tue, 21 May 2024 14:53:53 -0400 Subject: [PATCH 19/29] Improve robustness of compare() in the face of user-modification of AST. The comparison fundamentally depends on _fields and _attributes, which could be modified by the user. It's not clear that such modifications are sensible or supported by the API, but we can at least make sure comparison doesn't silently ignore those comparisons. Also pass a and b as arguments to helper methods instead of using them from the enclosing scope. --- Lib/ast.py | 14 ++++++++------ Lib/test/test_ast.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/Lib/ast.py b/Lib/ast.py index ced9dfce11fec3..6ea0524e00ed1b 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -441,7 +441,9 @@ def _compare(a, b): else: return type(a) is type(b) and a == b - def _compare_fields(): + 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) @@ -450,7 +452,9 @@ def _compare_fields(): else: return True - def _compare_attributes(): + def _compare_attributes(a, b): + if a._attributes != b._attributes: + return False # Attributes are always strings. for attr in a._attributes: a_attr = getattr(a, attr) @@ -462,11 +466,9 @@ def _compare_attributes(): if type(a) is not type(b): return False - # a and b are guaranteed to have the same type, so they must also - # have identical values for _fields and _attributes. - if not _compare_fields(): + if not _compare_fields(a, b): return False - if compare_attributes and not _compare_attributes(): + if compare_attributes and not _compare_attributes(a, b): return False return True diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index f91075533bde5b..535a8dfe2dcf1f 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -1077,6 +1077,36 @@ def test_compare_basics(self): ast.compare(ast.parse("x = 10;y = 20"), ast.parse("class C:pass")) ) + def test_compare_modified_ast(self): + # The ast API is a bit underspecified. The objects are mutable, + # and even _fields and _attributes are mutable. The compare() does + # some simple things to accommodate mutability. + a = ast.parse("m * x + b", mode="eval") + b = ast.parse("m * x + b", mode="eval") + self.assertTrue(ast.compare(a, b)) + + a._fields = a._fields + ("spam",) + a.spam = "Spam" + self.assertNotEqual(a._fields, b._fields) + self.assertFalse(ast.compare(a, b)) + self.assertFalse(ast.compare(b, a)) + + b._fields = a._fields + b.spam = "Spam" + self.assertTrue(ast.compare(a, b)) + self.assertTrue(ast.compare(b, a)) + + b._attributes = b._attributes + ("eggs",) + b.eggs = "eggs" + self.assertNotEqual(a._attributes, b._attributes) + self.assertFalse(ast.compare(a, b, compare_attributes=True)) + self.assertFalse(ast.compare(b, a, compare_attributes=True)) + + a._attributes = b._attributes + a.eggs = b.eggs + self.assertTrue(ast.compare(a, b, compare_attributes=True)) + self.assertTrue(ast.compare(b, a, compare_attributes=True)) + def test_compare_literals(self): constants = ( -20, From f9d4c392fc7b3a3df1139d8f9480b7f019059b8e Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Tue, 21 May 2024 15:04:32 -0400 Subject: [PATCH 20/29] Update Lib/test/test_ast.py Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> --- Lib/test/test_ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index 535a8dfe2dcf1f..4ec491f8974af5 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -1155,7 +1155,7 @@ def test_compare_fieldless(self): self.assertTrue(ast.compare(ast.Add(), ast.Add())) self.assertFalse(ast.compare(ast.Sub(), ast.Add())) - def test_compare_tests(self): + def test_compare_modes(self): for mode, sources in ( ("exec", exec_tests), ("eval", eval_tests), From ed0cddde276bdd5e15b3237a98b8947dc72a0e3c Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Tue, 21 May 2024 15:04:43 -0400 Subject: [PATCH 21/29] Update Lib/test/test_ast.py Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> --- Lib/test/test_ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index 4ec491f8974af5..45ca9b9aafd6e0 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -1168,7 +1168,7 @@ def test_compare_modes(self): ast.compare(a, b), f"{ast.dump(a)} != {ast.dump(b)}" ) - def test_compare_options(self): + def test_compare_attributes_option(self): def parse(a, b): return ast.parse(a), ast.parse(b) From 2b114a95d6674968188643c8e4d85880a6d9eb39 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Tue, 21 May 2024 15:19:41 -0400 Subject: [PATCH 22/29] Update Lib/test/test_ast.py Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> --- Lib/test/test_ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index 45ca9b9aafd6e0..ba25664faa3e48 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -1092,7 +1092,7 @@ def test_compare_modified_ast(self): self.assertFalse(ast.compare(b, a)) b._fields = a._fields - b.spam = "Spam" + b.spam = a.spam self.assertTrue(ast.compare(a, b)) self.assertTrue(ast.compare(b, a)) From 4687dc657cc64b2ffa9d80e034b321b251ff1c35 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Tue, 21 May 2024 15:20:48 -0400 Subject: [PATCH 23/29] Update Lib/test/test_ast.py Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> --- Lib/test/test_ast.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index ba25664faa3e48..8a4374c56cbc08 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -1173,6 +1173,7 @@ def parse(a, b): return ast.parse(a), ast.parse(b) a, b = parse("2 + 2", "2+2") + self.assertTrue(ast.compare(a, b)) self.assertTrue(ast.compare(a, b, compare_attributes=False)) self.assertFalse(ast.compare(a, b, compare_attributes=True)) From 455bdb103c3fa45274f55d8e50411439d084b45e Mon Sep 17 00:00:00 2001 From: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> Date: Tue, 21 May 2024 15:22:30 -0400 Subject: [PATCH 24/29] whitespace --- .../NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst b/Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst index 675f5dae0918f8..b906393449656d 100644 --- a/Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst +++ b/Misc/NEWS.d/next/Library/2020-03-28-21-00-54.bpo-15987.aBL8XS.rst @@ -1,3 +1,2 @@ Implemented :func:`ast.compare` for comparing two ASTs. Patch by Batuhan Taskaya with some help from Jeremy Hylton. - From ccf410cf69b0af885f66211c34c6587965443495 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Tue, 21 May 2024 15:55:33 -0400 Subject: [PATCH 25/29] Update Doc/library/ast.rst Co-authored-by: Jelle Zijlstra --- Doc/library/ast.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index 88c0e6799ff4f5..d638504994068f 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -2477,7 +2477,7 @@ effects on the compilation of a program: Recursively compares two ASTs. *compare_attributes* affects whether AST attributes are considered - in the comparison. If compare_attributes is ``False`` (default), then + in the comparison. If *compare_attributes* is ``False`` (default), then attributes are ignored. Otherwise they must all be equal. This option is useful to check whether the ASTs are structurally equal but might differ in whitespace or similar details. From 06988c194f0690c8bcd3517c874c7e8cb103eb71 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Tue, 21 May 2024 15:56:01 -0400 Subject: [PATCH 26/29] Update Lib/ast.py Co-authored-by: Jelle Zijlstra --- Lib/ast.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/ast.py b/Lib/ast.py index 6ea0524e00ed1b..fc05ba13a9da4d 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -426,7 +426,6 @@ def _compare(a, b): a, b, compare_attributes=compare_attributes, - ) elif isinstance(a, list): # If a field is repeated, then both objects will represent From 0c9da18f21d2424e1a3e06ae7ee1db3b1dcf4d92 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Tue, 21 May 2024 15:58:01 -0400 Subject: [PATCH 27/29] Attributes are ints not strings. --- Lib/ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ast.py b/Lib/ast.py index fc05ba13a9da4d..031bab43df7579 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -454,7 +454,7 @@ def _compare_fields(a, b): def _compare_attributes(a, b): if a._attributes != b._attributes: return False - # Attributes are always strings. + # Attributes are always ints. for attr in a._attributes: a_attr = getattr(a, attr) b_attr = getattr(b, attr) From 6494c2391a1d9848ab4696483ed53ccdb064042e Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Tue, 21 May 2024 15:59:50 -0400 Subject: [PATCH 28/29] Explain examples of what are included in attributes. --- Doc/library/ast.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index d638504994068f..3f3c568f5807c5 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -2480,7 +2480,8 @@ effects on the compilation of a program: in the comparison. If *compare_attributes* is ``False`` (default), then attributes are ignored. Otherwise they must all be equal. This option is useful to check whether the ASTs are structurally equal but - might differ in whitespace or similar details. + differ in whitespace or similar details. Attributes include numbers + and column offsets. .. versionadded:: 3.14 From bf9e40387720570ec1a92d7ae8a713cd42c32128 Mon Sep 17 00:00:00 2001 From: Jeremy Hylton <32469542+jeremyhylton@users.noreply.github.com> Date: Tue, 21 May 2024 16:11:51 -0400 Subject: [PATCH 29/29] Update Doc/library/ast.rst Co-authored-by: Jelle Zijlstra --- Doc/library/ast.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index 3f3c568f5807c5..9ee56b92431b57 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -2480,7 +2480,7 @@ effects on the compilation of a program: in the comparison. If *compare_attributes* is ``False`` (default), then attributes are ignored. Otherwise they must all be equal. This option is useful to check whether the ASTs are structurally equal but - differ in whitespace or similar details. Attributes include numbers + differ in whitespace or similar details. Attributes include line numbers and column offsets. .. versionadded:: 3.14