diff --git a/Lib/lib2to3/fixer_util.py b/Lib/lib2to3/fixer_util.py index c2a3a47f503286e..66d9204b349115f 100644 --- a/Lib/lib2to3/fixer_util.py +++ b/Lib/lib2to3/fixer_util.py @@ -150,6 +150,38 @@ def ImportAndCall(node, results, names): new.prefix = node.prefix return new +def BlankLineOrPass(node): + """Returns either a blank line or a pass statement depending on + the node's parent's siblings to maintain syntactic correctness + within a suite after conversion""" + skip = {token.NEWLINE, token.INDENT, token.DEDENT} + def has_significant_sibling(node, is_forward): + if is_forward: + sibling = node.next_sibling + else: + sibling = node.prev_sibling + while sibling: + if isinstance(sibling, Node): + if sibling.type != syms.simple_stmt: + return True + for child in sibling.children: + if child.type not in skip: + return True + elif isinstance(sibling, Leaf) and sibling.type not in skip: + return True + if is_forward: + sibling = sibling.next_sibling + else: + sibling = sibling.prev_sibling + return False + + parent = node.parent + if parent and parent.parent and parent.parent.type == syms.suite: + if (not has_significant_sibling(parent, False) + and not has_significant_sibling(parent, True)): + return Name("pass") + return BlankLine() + ########################################################### ### Determine whether a node represents a given literal diff --git a/Lib/lib2to3/fixes/fix_future.py b/Lib/lib2to3/fixes/fix_future.py index fbcb86af0791338..cd8e25cccc152b6 100644 --- a/Lib/lib2to3/fixes/fix_future.py +++ b/Lib/lib2to3/fixes/fix_future.py @@ -6,7 +6,7 @@ # Local imports from .. import fixer_base -from ..fixer_util import BlankLine +from ..fixer_util import BlankLineOrPass class FixFuture(fixer_base.BaseFix): BM_compatible = True @@ -17,6 +17,6 @@ class FixFuture(fixer_base.BaseFix): run_order = 10 def transform(self, node, results): - new = BlankLine() + new = BlankLineOrPass(node) new.prefix = node.prefix return new diff --git a/Lib/lib2to3/fixes/fix_itertools_imports.py b/Lib/lib2to3/fixes/fix_itertools_imports.py index 0ddbc7b8422991b..8007dd81423585d 100644 --- a/Lib/lib2to3/fixes/fix_itertools_imports.py +++ b/Lib/lib2to3/fixes/fix_itertools_imports.py @@ -2,7 +2,7 @@ # Local imports from lib2to3 import fixer_base -from lib2to3.fixer_util import BlankLine, syms, token +from lib2to3.fixer_util import BlankLineOrPass, syms, token class FixItertoolsImports(fixer_base.BaseFix): @@ -52,6 +52,6 @@ def transform(self, node, results): if (not (imports.children or getattr(imports, 'value', None)) or imports.parent is None): p = node.prefix - node = BlankLine() + node = BlankLineOrPass(node) node.prefix = p return node diff --git a/Lib/lib2to3/tests/test_fixers.py b/Lib/lib2to3/tests/test_fixers.py index 3da5dd845c93c66..81da057a0168175 100644 --- a/Lib/lib2to3/tests/test_fixers.py +++ b/Lib/lib2to3/tests/test_fixers.py @@ -25,15 +25,16 @@ def setUp(self, fix_list=None, fixer_pkg="lib2to3", options=None): self.refactor.post_order): fixer.log = self.fixer_log - def _check(self, before, after): - before = support.reformat(before) - after = support.reformat(after) + def _check(self, before, after, reformat=True): + if reformat: + before = support.reformat(before) + after = support.reformat(after) tree = self.refactor.refactor_string(before, self.filename) self.assertEqual(after, str(tree)) return tree - def check(self, before, after, ignore_warnings=False): - tree = self._check(before, after) + def check(self, before, after, ignore_warnings=False, reformat=True): + tree = self._check(before, after, reformat=reformat) self.assertTrue(tree.was_changed) if not ignore_warnings: self.assertEqual(self.fixer_log, []) @@ -3656,9 +3657,130 @@ def test_future(self): a = """\n# comment""" self.check(b, a) + def test_suite_try_blank(self): + b = ( + "try:\n" + " from __future__ import with_statement\n" + " from sys import exit\n" + "except ImportError:\n" + " pass\n") + a = ( + "try:\n" + " \n" + " from sys import exit\n" + "except ImportError:\n" + " pass\n") + self.check(b, a, reformat=False) # to avoid dedent + + def test_suite_try_pass(self): + b = ( + "try:\n" + " from __future__ import with_statement\n" + "except ImportError:\n" + " pass\n") + a = ( + "try:\n" + " pass\n" + "except ImportError:\n" + " pass\n") + self.check(b, a) + + def test_suite_if_blank(self): + b = ( + "if sys.version_info < (3, 0):\n" + " from __future__ import with_statement\n" + " from sys import exit\n") + a = ( + "if sys.version_info < (3, 0):\n" + " \n" + " from sys import exit\n") + self.check(b, a, reformat=False) # to avoid dedent + + def test_suite_if_pass(self): + b = ( + "if sys.version_info < (3, 0):\n" + " from __future__ import with_statement\n") + a = ( + "if sys.version_info < (3, 0):\n" + " pass\n") + self.check(b, a) # to avoid dedent + + def test_pass_with_comments(self): + b = ( + "try:\n" + " # this comment\n" + " from __future__ import with_statement # that comment\n" + "except ImportError:\n" + " pass\n") + a = ( + "try:\n" + " # this comment\n" + " pass # that comment\n" + "except ImportError:\n" + " pass\n") + self.check(b, a) + + def test_pass_with_newlines(self): + b = ( + "try:\n" + " \n" + " \n" + " from __future__ import with_statement\n" + " \n" + "except ImportError:\n" + " pass\n") + a = ( + "try:\n" + " \n" + " \n" + " pass\n" + " \n" + "except ImportError:\n" + " pass\n") + self.check(b, a) + def test_run_order(self): self.assert_runs_after('print') + +class Test_future_with_itertools_imports(FixerTestCase): + + def setUp(self): + fix_list = ["future", "itertools_imports"] + super(Test_future_with_itertools_imports, self).setUp(fix_list) + + def test_double_transform(self): + """Note the difference between the two conversion results, due to + the fact that 'future' fixer runs last""" + b = ( + "try:\n" + " from __future__ import with_statement\n" + " from itertools import imap\n" + "except ImportError:\n" + " pass\n") + a = ( + "try:\n" + " pass\n" + " \n" + "except ImportError:\n" + " pass\n") + self.check(b, a, reformat=False) # to avoid dedent + + b = ( + "try:\n" + " from itertools import imap\n" + " from __future__ import with_statement\n" + "except ImportError:\n" + " pass\n") + a = ( + "try:\n" + " \n" + " pass\n" + "except ImportError:\n" + " pass\n") + self.check(b, a, reformat=False) # to avoid dedent + + class Test_itertools(FixerTestCase): fixer = "itertools" @@ -3784,11 +3906,92 @@ def test_ifilter_and_zip_longest(self): a = "from itertools import bar, %s, foo" % (name,) self.check(b, a) + def test_suite_try_blank(self): + b = ( + "try:\n" + " from itertools import imap\n" + " from sys import exit\n" + "except ImportError:\n" + " pass\n") + a = ( + "try:\n" + " \n" + " from sys import exit\n" + "except ImportError:\n" + " pass\n") + self.check(b, a, reformat=False) # to avoid dedent + + def test_suite_try_pass(self): + b = ( + "try:\n" + " from itertools import imap\n" + "except ImportError:\n" + " pass\n") + a = ( + "try:\n" + " pass\n" + "except ImportError:\n" + " pass\n") + self.check(b, a) + + def test_suite_if_blank(self): + b = ( + "if sys.version_info < (3, 0):\n" + " from itertools import imap\n" + " from sys import exit\n") + a = ( + "if sys.version_info < (3, 0):\n" + " \n" + " from sys import exit\n") + self.check(b, a, reformat=False) # to avoid dedent + + def test_suite_if_pass(self): + b = ( + "if sys.version_info < (3, 0):\n" + " from itertools import imap\n") + a = ( + "if sys.version_info < (3, 0):\n" + " pass\n") + self.check(b, a) # to avoid dedent + + def test_pass_with_comments(self): + b = ( + "try:\n" + " # this comment\n" + " from itertools import imap # that comment\n" + "except ImportError:\n" + " pass\n") + a = ( + "try:\n" + " # this comment\n" + " pass # that comment\n" + "except ImportError:\n" + " pass\n") + self.check(b, a) + + def test_pass_with_newlines(self): + b = ( + "try:\n" + " \n" + " \n" + " from itertools import imap\n" + " \n" + "except ImportError:\n" + " pass\n") + a = ( + "try:\n" + " \n" + " \n" + " pass\n" + " \n" + "except ImportError:\n" + " pass\n") + self.check(b, a) + def test_import_star(self): s = "from itertools import *" self.unchanged(s) - def test_unchanged(self): s = "from itertools import foo" self.unchanged(s) diff --git a/Misc/NEWS.d/next/Library/2019-11-09-06-16-24.bpo-38681.jJobMS.rst b/Misc/NEWS.d/next/Library/2019-11-09-06-16-24.bpo-38681.jJobMS.rst new file mode 100644 index 000000000000000..5250d5a4ed00443 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-11-09-06-16-24.bpo-38681.jJobMS.rst @@ -0,0 +1,2 @@ +Transformation performed by certain fixers (e.g. future, itertools_imports) that causes a statement to be replaced by a blank line will generate a Python file that contains a syntax error. Enhancement applied checks whether the statement to be replaced has any siblings or not. If no sibling is found, then the statement gets replaced with a "pass" statement instead of a blank line. +By doing this, Python source files generated by 2to3 are more readily runnable right after the transformation.