diff --git a/rope/base/codeanalyze.py b/rope/base/codeanalyze.py index 1704e9ade..75ebd1ec8 100644 --- a/rope/base/codeanalyze.py +++ b/rope/base/codeanalyze.py @@ -350,13 +350,22 @@ def count_line_indents(line): return 0 -def get_string_pattern(): - start = r'(\b[uU]?[rR]?)?' - longstr = r'%s"""(\\.|"(?!"")|\\\n|[^"\\])*"""' % start - shortstr = r'%s"(\\.|\\\n|[^"\\])*"' % start +def get_string_pattern_with_prefix(prefix): + longstr = r'%s"""(\\.|"(?!"")|\\\n|[^"\\])*"""' % prefix + shortstr = r'%s"(\\.|\\\n|[^"\\])*"' % prefix return '|'.join([longstr, longstr.replace('"', "'"), shortstr, shortstr.replace('"', "'")]) +def get_string_pattern(): + prefix = r'(?= (2, 7) PY3 = sys.version_info[0] == 3 PY34 = sys.version_info[0:2] >= (3, 4) +PY36 = sys.version_info[0:2] >= (3, 6) try: str = unicode diff --git a/rope/refactor/occurrences.py b/rope/refactor/occurrences.py index dfc2d685d..26bedc7fc 100644 --- a/rope/refactor/occurrences.py +++ b/rope/refactor/occurrences.py @@ -35,6 +35,7 @@ arguments """ +import ast import re from rope.base import codeanalyze @@ -300,6 +301,8 @@ def __init__(self, name, docs=False): self.comment_pattern = _TextualFinder.any('comment', [r'#[^\n]*']) self.string_pattern = _TextualFinder.any( 'string', [codeanalyze.get_string_pattern()]) + self.f_string_pattern = _TextualFinder.any( + 'fstring', [codeanalyze.get_formatted_string_pattern()]) self.pattern = self._get_occurrence_pattern(self.name) def find_offsets(self, source): @@ -314,9 +317,18 @@ def find_offsets(self, source): def _re_search(self, source): for match in self.pattern.finditer(source): - for key, value in match.groupdict().items(): - if value and key == 'occurrence': - yield match.start(key) + if match.groupdict()['occurrence']: + yield match.start('occurrence') + elif utils.pycompat.PY36 and match.groupdict()['fstring']: + f_string = match.groupdict()['fstring'] + for occurrence_node in self._search_in_f_string(f_string): + yield match.start('fstring') + occurrence_node.col_offset + + def _search_in_f_string(self, f_string): + tree = ast.parse(f_string) + for node in ast.walk(tree): + if isinstance(node, ast.Name) and node.id == self.name: + yield node def _normal_search(self, source): current = 0 @@ -352,7 +364,8 @@ def _get_occurrence_pattern(self, name): occurrence_pattern = _TextualFinder.any('occurrence', ['\\b' + name + '\\b']) pattern = re.compile(occurrence_pattern + '|' + self.comment_pattern + - '|' + self.string_pattern) + '|' + self.string_pattern + '|' + + self.f_string_pattern) return pattern @staticmethod diff --git a/ropetest/refactor/renametest.py b/ropetest/refactor/renametest.py index f279f6ef1..92911e11e 100644 --- a/ropetest/refactor/renametest.py +++ b/ropetest/refactor/renametest.py @@ -106,7 +106,7 @@ def test_renaming_function_parameters_of_class_init(self): 'a_var = A(new_param=1)\n' self.assertEquals(expected, refactored) - def test_renam_functions_parameters_and_occurances_in_other_modules(self): + def test_rename_functions_parameters_and_occurences_in_other_modules(self): mod1 = testutils.create_module(self.project, 'mod1') mod2 = testutils.create_module(self.project, 'mod2') mod1.write('def a_func(a_param):\n print(a_param)\n') @@ -123,6 +123,28 @@ def test_renaming_with_backslash_continued_names(self): self.assertEquals("is_replace = True\n'ali'.\\\nreplace\n", refactored) + @testutils.only_for('3.6') + def test_renaming_occurrence_in_f_string(self): + refactored = self._local_rename( + "a_var = 20\na_string=f'value: {a_var}'\n", 2, 'new_var') + self.assertEquals("new_var = 20\na_string=f'value: {new_var}'\n", + refactored) + + @testutils.only_for('3.6') + def test_renaming_occurrence_in_nested_f_string(self): + refactored = self._local_rename( + "a_var = 20\na_string=f'{f\"{a_var}\"}'\n", 2, 'new_var') + self.assertEquals( + "new_var = 20\na_string=f'{f\"{new_var}\"}'\n", + refactored) + + @testutils.only_for('3.6') + def test_not_renaming_string_contents_in_f_string(self): + refactored = self._local_rename( + "a_var = 20\na_string=f'{\"a_var\"}'\n", 2, 'new_var') + self.assertEquals("new_var = 20\na_string=f'{\"a_var\"}'\n", + refactored) + def test_not_renaming_string_contents(self): refactored = self._local_rename("a_var = 20\na_string='a_var'\n", 2, 'new_var')