Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for ast.Attributes when finding occurrences in f-strings #751

Merged
merged 5 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
18 changes: 11 additions & 7 deletions rope/refactor/occurrences.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

import contextlib
import re
from typing import Iterator

from rope.base import (
ast,
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
22 changes: 22 additions & 0 deletions ropetest/refactor/renametest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading