diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b412449..cfa7baf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # **Upcoming release** +- Check for ast.Attributes when finding occurrences in fstrings (@sandratsy) - #777, #698 add validation to refuse Rename refactoring to a python keyword - #730 Match on module aliases for autoimport suggestions - #755 Remove dependency on `build` package being installed while running tests diff --git a/rope/refactor/occurrences.py b/rope/refactor/occurrences.py index 5839cf3d..b136cc5c 100644 --- a/rope/refactor/occurrences.py +++ b/rope/refactor/occurrences.py @@ -37,6 +37,7 @@ import contextlib import re +from typing import Iterator from rope.base import ( ast, @@ -319,7 +320,7 @@ def __init__(self, name, docs=False): ) self.pattern = self._get_occurrence_pattern(self.name) - def find_offsets(self, source): + def find_offsets(self, source: str) -> Iterator[int]: if not self._fast_file_query(source): return if self.docs: @@ -328,22 +329,25 @@ def find_offsets(self, source): searcher = self._re_search yield from searcher(source) - def _re_search(self, source): + def _re_search(self, source: str) -> Iterator[int]: for match in self.pattern.finditer(source): if match.groupdict()["occurrence"]: yield match.start("occurrence") elif 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 + for offset in self._search_in_f_string(f_string): + yield match.start("fstring") + offset - def _search_in_f_string(self, f_string): + def _search_in_f_string(self, f_string: str) -> Iterator[int]: tree = ast.parse(f_string) for node in ast.walk(tree): if isinstance(node, ast.Name) and node.id == self.name: - yield node + yield node.col_offset + elif isinstance(node, ast.Attribute) and node.attr == self.name: + assert node.end_col_offset is not None + yield node.end_col_offset - len(self.name) - def _normal_search(self, source): + def _normal_search(self, source: str) -> Iterator[int]: current = 0 while True: try: diff --git a/ropetest/refactor/renametest.py b/ropetest/refactor/renametest.py index a49e3e74..78bc38d8 100644 --- a/ropetest/refactor/renametest.py +++ b/ropetest/refactor/renametest.py @@ -331,6 +331,28 @@ def test_renaming_occurrence_in_nested_f_string(self): refactored = self._local_rename(code, 2, "new_var") self.assertEqual(expected, refactored) + def test_renaming_attribute_occurrences_in_f_string(self): + code = dedent("""\ + class MyClass: + def __init__(self): + self.abc = 123 + + def func(obj): + print(f'{obj.abc}') + return obj.abc + """) + expected = dedent("""\ + class MyClass: + def __init__(self): + self.new_var = 123 + + def func(obj): + print(f'{obj.new_var}') + return obj.new_var + """) + refactored = self._local_rename(code, code.index('abc'), "new_var") + self.assertEqual(expected, refactored) + 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"