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))