diff --git a/.all-contributorsrc b/.all-contributorsrc index 475d2e590..14a0d24ee 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -481,6 +481,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/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 + 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

🐛 diff --git a/rope/base/codeanalyze.py b/rope/base/codeanalyze.py index bb0d988e3..3e2e0f618 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('"', "'"), + ] + ), ) @@ -369,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..ac5acce26 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,18 @@ 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..fb857da5e 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,43 @@ 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) + + @testutils.only_for_versions_higher("3.6") + 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) + + @testutils.only_for_versions_higher("3.6") + 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..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("")) @@ -54,3 +53,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))