From 8150711a1855a40425a529f593d8a46a629020cc Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Sun, 26 Sep 2021 07:58:23 +1000 Subject: [PATCH 1/6] Allow retrieving prefix as match group in get_string_pattern_with_prefix() --- rope/base/codeanalyze.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/rope/base/codeanalyze.py b/rope/base/codeanalyze.py index bb0d988e3..db043a4b5 100644 --- a/rope/base/codeanalyze.py +++ b/rope/base/codeanalyze.py @@ -351,11 +351,23 @@ def count_line_indents(line): return 0 -def get_string_pattern_with_prefix(prefix): - longstr = r'%s"""(\\.|"(?!"")|\\\n|[^"\\])*"""' % prefix - shortstr = r'%s"(\\.|\\\n|[^"\\\n])*"' % prefix - return "|".join( - [longstr, longstr.replace('"', "'"), shortstr, shortstr.replace('"', "'")] +def get_string_pattern_with_prefix(prefix, prefix_group_name=None): + longstr = r'"""(\\.|"(?!"")|\\\n|[^"\\])*"""' + shortstr = r'"(\\.|\\\n|[^"\\\n])*"' + if prefix_group_name is not None: + pattern = "(?P<%s>%%s)(%%s)" % prefix_group_name + else: + pattern = "%s(%s)" + return pattern % ( + prefix, + "|".join( + [ + longstr, + longstr.replace('"', "'"), + shortstr, + shortstr.replace('"', "'"), + ] + ), ) From 56b23120b9442425db7f929248f8a5a3f153e852 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Mon, 27 Sep 2021 11:15:48 +1000 Subject: [PATCH 2/6] Improve simplification of f-string Previously, if there are quotes inside f-string like so: s = f' test "{hello}" test' the simplifier will match and simplify "{hello}", producing the following simplified text: s = f' test " " test' This change fixes that and prevents simplification in f-strings, as they may contain symbols that we need to scan for during inlining. --- rope/base/codeanalyze.py | 8 ++++++++ rope/base/simplify.py | 16 ++++++++++++---- ropetest/refactor/inlinetest.py | 32 ++++++++++++++++++++++++++++++++ ropetest/simplifytest.py | 12 ++++++++++++ 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/rope/base/codeanalyze.py b/rope/base/codeanalyze.py index db043a4b5..7f0f9c457 100644 --- a/rope/base/codeanalyze.py +++ b/rope/base/codeanalyze.py @@ -381,5 +381,13 @@ def get_formatted_string_pattern(): return get_string_pattern_with_prefix(prefix) +def get_any_string_pattern(): + prefix = r'[bBfFrRuU]{,4}' + return get_string_pattern_with_prefix( + prefix, + prefix_group_name="prefix", + ) + + def get_comment_pattern(): return r"#[^\n]*" diff --git a/rope/base/simplify.py b/rope/base/simplify.py index eb7b4332c..393290a38 100644 --- a/rope/base/simplify.py +++ b/rope/base/simplify.py @@ -22,12 +22,15 @@ def real_code(source): only in offsets. """ collector = codeanalyze.ChangeCollector(source) - for start, end in ignored_regions(source): + for start, end, matchgroups in ignored_regions(source): if source[start] == "#": replacement = " " * (end - start) + elif 'f' in matchgroups.get('prefix', '').lower(): + replacement = None else: replacement = '"%s"' % (" " * (end - start - 2)) - collector.add_change(start, end, replacement) + if replacement is not None: + collector.add_change(start, end, replacement) source = collector.get_changed() or source collector = codeanalyze.ChangeCollector(source) parens = 0 @@ -47,10 +50,15 @@ def real_code(source): @utils.cached(7) def ignored_regions(source): """Return ignored regions like strings and comments in `source`""" - return [(match.start(), match.end()) for match in _str.finditer(source)] + return [(match.start(), match.end(), match.groupdict()) for match in _str.finditer(source)] _str = re.compile( - "%s|%s" % (codeanalyze.get_comment_pattern(), codeanalyze.get_string_pattern()) + "|".join( + [ + codeanalyze.get_comment_pattern(), + codeanalyze.get_any_string_pattern(), + ] + ) ) _parens = re.compile(r"[\({\[\]}\)\n]") diff --git a/ropetest/refactor/inlinetest.py b/ropetest/refactor/inlinetest.py index b7ce85e25..a68e46718 100644 --- a/ropetest/refactor/inlinetest.py +++ b/ropetest/refactor/inlinetest.py @@ -1,3 +1,5 @@ +from textwrap import dedent + try: import unittest2 as unittest except ImportError: @@ -669,3 +671,33 @@ def test_inlining_does_change_string_constants_if_docs_is_set(self): code, code.rindex("var"), remove=False, only_current=True, docs=True ) self.assertEqual(expected, refactored) + + def test_inlining_into_format_string(self): + code = dedent("""\ + var = 123 + print(f"{var}") + """) + expected = dedent("""\ + print(f"{123}") + """) + + refactored = self._inline(code, code.rindex("var")) + + self.assertEqual(expected, refactored) + + def test_inlining_into_format_string_containing_quotes(self): + code = dedent('''\ + var = 123 + print(f" '{var}' ") + print(f""" "{var}" """) + print(f' "{var}" ') + ''') + expected = dedent('''\ + print(f" '{123}' ") + print(f""" "{123}" """) + print(f' "{123}" ') + ''') + + refactored = self._inline(code, code.rindex("var")) + + self.assertEqual(expected, refactored) diff --git a/ropetest/simplifytest.py b/ropetest/simplifytest.py index 94274bb35..9ca5526ee 100644 --- a/ropetest/simplifytest.py +++ b/ropetest/simplifytest.py @@ -54,3 +54,15 @@ def test_replacing_tabs(self): def test_replacing_semicolons(self): code = "a = 1;b = 2\n" self.assertEqual("a = 1\nb = 2\n", simplify.real_code(code)) + + def test_simplifying_f_string(self): + code = 's = f"..{hello}.."\n' + self.assertEqual('s = f"..{hello}.."\n', simplify.real_code(code)) + + def test_simplifying_f_string_containing_quotes(self): + code = '''s = f"..'{hello}'.."\n''' + self.assertEqual('''s = f"..'{hello}'.."\n''', simplify.real_code(code)) + + def test_simplifying_uppercase_f_string_containing_quotes(self): + code = '''s = Fr"..'{hello}'.."\n''' + self.assertEqual('''s = Fr"..'{hello}'.."\n''', simplify.real_code(code)) From 8b18cc1ec68a346e4ce5d65b25380cbd853e1895 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Mon, 27 Sep 2021 11:24:37 +1000 Subject: [PATCH 3/6] Black formatting --- rope/base/codeanalyze.py | 2 +- rope/base/simplify.py | 7 +++++-- ropetest/refactor/inlinetest.py | 24 ++++++++++++++++-------- ropetest/simplifytest.py | 9 ++++----- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/rope/base/codeanalyze.py b/rope/base/codeanalyze.py index 7f0f9c457..3e2e0f618 100644 --- a/rope/base/codeanalyze.py +++ b/rope/base/codeanalyze.py @@ -382,7 +382,7 @@ def get_formatted_string_pattern(): def get_any_string_pattern(): - prefix = r'[bBfFrRuU]{,4}' + prefix = r"[bBfFrRuU]{,4}" return get_string_pattern_with_prefix( prefix, prefix_group_name="prefix", diff --git a/rope/base/simplify.py b/rope/base/simplify.py index 393290a38..ac5acce26 100644 --- a/rope/base/simplify.py +++ b/rope/base/simplify.py @@ -25,7 +25,7 @@ def real_code(source): for start, end, matchgroups in ignored_regions(source): if source[start] == "#": replacement = " " * (end - start) - elif 'f' in matchgroups.get('prefix', '').lower(): + elif "f" in matchgroups.get("prefix", "").lower(): replacement = None else: replacement = '"%s"' % (" " * (end - start - 2)) @@ -50,7 +50,10 @@ def real_code(source): @utils.cached(7) def ignored_regions(source): """Return ignored regions like strings and comments in `source`""" - return [(match.start(), match.end(), match.groupdict()) for match in _str.finditer(source)] + return [ + (match.start(), match.end(), match.groupdict()) + for match in _str.finditer(source) + ] _str = re.compile( diff --git a/ropetest/refactor/inlinetest.py b/ropetest/refactor/inlinetest.py index a68e46718..d03ce377b 100644 --- a/ropetest/refactor/inlinetest.py +++ b/ropetest/refactor/inlinetest.py @@ -673,30 +673,38 @@ def test_inlining_does_change_string_constants_if_docs_is_set(self): self.assertEqual(expected, refactored) def test_inlining_into_format_string(self): - code = dedent("""\ + code = dedent( + """\ var = 123 print(f"{var}") - """) - expected = dedent("""\ + """ + ) + expected = dedent( + """\ print(f"{123}") - """) + """ + ) refactored = self._inline(code, code.rindex("var")) self.assertEqual(expected, refactored) def test_inlining_into_format_string_containing_quotes(self): - code = dedent('''\ + code = dedent( + '''\ var = 123 print(f" '{var}' ") print(f""" "{var}" """) print(f' "{var}" ') - ''') - expected = dedent('''\ + ''' + ) + expected = dedent( + '''\ print(f" '{123}' ") print(f""" "{123}" """) print(f' "{123}" ') - ''') + ''' + ) refactored = self._inline(code, code.rindex("var")) diff --git a/ropetest/simplifytest.py b/ropetest/simplifytest.py index 9ca5526ee..dcaa8c456 100644 --- a/ropetest/simplifytest.py +++ b/ropetest/simplifytest.py @@ -7,7 +7,6 @@ class SimplifyTest(unittest.TestCase): - def test_trivial_case(self): self.assertEqual("", simplify.real_code("")) @@ -60,9 +59,9 @@ def test_simplifying_f_string(self): self.assertEqual('s = f"..{hello}.."\n', simplify.real_code(code)) def test_simplifying_f_string_containing_quotes(self): - code = '''s = f"..'{hello}'.."\n''' - self.assertEqual('''s = f"..'{hello}'.."\n''', simplify.real_code(code)) + code = """s = f"..'{hello}'.."\n""" + self.assertEqual("""s = f"..'{hello}'.."\n""", simplify.real_code(code)) def test_simplifying_uppercase_f_string_containing_quotes(self): - code = '''s = Fr"..'{hello}'.."\n''' - self.assertEqual('''s = Fr"..'{hello}'.."\n''', simplify.real_code(code)) + code = """s = Fr"..'{hello}'.."\n""" + self.assertEqual("""s = Fr"..'{hello}'.."\n""", simplify.real_code(code)) From cbdd139876e8997afca6c07b16ca79670898f096 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Mon, 27 Sep 2021 11:28:39 +1000 Subject: [PATCH 4/6] Set minimum Python version for test --- ropetest/refactor/inlinetest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ropetest/refactor/inlinetest.py b/ropetest/refactor/inlinetest.py index d03ce377b..fb857da5e 100644 --- a/ropetest/refactor/inlinetest.py +++ b/ropetest/refactor/inlinetest.py @@ -672,6 +672,7 @@ def test_inlining_does_change_string_constants_if_docs_is_set(self): ) self.assertEqual(expected, refactored) + @testutils.only_for_versions_higher("3.6") def test_inlining_into_format_string(self): code = dedent( """\ @@ -689,6 +690,7 @@ def test_inlining_into_format_string(self): self.assertEqual(expected, refactored) + @testutils.only_for_versions_higher("3.6") def test_inlining_into_format_string_containing_quotes(self): code = dedent( '''\ From 1624d2bfb076ea50c51c2ca0c80a233c33bbe172 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Mon, 27 Sep 2021 11:57:30 +1000 Subject: [PATCH 5/6] Add contributors --- .all-contributorsrc | 9 +++++++++ CONTRIBUTORS.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 0c73f6660..7f6dcc463 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -482,6 +482,15 @@ "contributions": [ "bug" ] + }, + { + "login": "mlao-pdx", + "name": "MLAO", + "avatar_url": "https://avatars.githubusercontent.com/u/21014310?v=4", + "profile": "https://github.com/mlao-pdx", + "contributions": [ + "bug" + ] } ], "projectName": "rope", diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 719141858..4d31ca082 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -72,6 +72,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Brendan Maginnis

💻
Alexander Solovyov

💻
MATSUI Tetsushi

🐛 +
MLAO

🐛 From 823ec40219e9b45e0a1274bf0ca3ab67db592618 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Mon, 27 Sep 2021 11:58:43 +1000 Subject: [PATCH 6/6] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8780d1843..cf3d5a966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ## Bug fixes - #391, #396 Extract method similar no longer replace the left-hand side of assignment +- #303 Fix inlining into f-string containing quote characters +