From 22a54c208f00cbb7dd1065259a0b9dc5457540fd Mon Sep 17 00:00:00 2001 From: aliassar Date: Tue, 10 Nov 2026 21:07:07 +0100 Subject: [PATCH] Don't rename string prefixes that match the renamed name The textual occurrence pattern matched `\bname\b`, so for a variable called `f` it also matched the `f` of an f-string prefix (and likewise `b`, `u`, `rf`, ...). Rename rewrote `f"{f}"` into `g"{g}"` and inline produced `1"{1}"`, both syntax errors. A name directly followed by a quote can only be a string prefix, so skip those matches and let the string patterns consume the literal. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + rope/refactor/occurrences.py | 6 +++++- ropetest/refactor/renametest.py | 24 ++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba328bd9a..65f04b617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - #119, #928 Fix IndexError in rename and find occurrences when a `from` import is the last thing in a module without a trailing newline (@mcepl) - #921 Patch the else suite of try statements that also have a finally (@marlon-costa-dc) - #937 Preserve non-ASCII argument expressions and default values when changing function signatures (@Jinyong-Li) +- #948, #949 Don't treat string prefixes such as the `f` in `f"..."` as occurrences when renaming a variable named `f`, `b`, `u`, `rf`, etc. (@aliassar) # Release 1.15.1 diff --git a/rope/refactor/occurrences.py b/rope/refactor/occurrences.py index b136cc5c9..6bc3555bf 100644 --- a/rope/refactor/occurrences.py +++ b/rope/refactor/occurrences.py @@ -373,7 +373,11 @@ def _get_source(self, resource, pymodule): return pymodule.source_code def _get_occurrence_pattern(self, name): - occurrence_pattern = _TextualFinder.any("occurrence", ["\\b" + name + "\\b"]) + # A name directly followed by a quote is a string prefix, e.g. the + # ``f`` in ``f"..."``, never a reference to the name. + occurrence_pattern = _TextualFinder.any( + "occurrence", ["\\b" + name + "\\b(?![\"'])"] + ) pattern = re.compile( occurrence_pattern + "|" diff --git a/ropetest/refactor/renametest.py b/ropetest/refactor/renametest.py index a92883693..7f24ece50 100644 --- a/ropetest/refactor/renametest.py +++ b/ropetest/refactor/renametest.py @@ -353,6 +353,30 @@ def func(obj): refactored = self._local_rename(code, code.index('abc'), "new_var") self.assertEqual(expected, refactored) + def test_not_renaming_f_string_prefix(self): + code = dedent("""\ + f = open("out.txt", "w") + f.write(f"{f.name}") + """) + expected = dedent("""\ + fh = open("out.txt", "w") + fh.write(f"{fh.name}") + """) + refactored = self._local_rename(code, 0, "fh") + self.assertEqual(expected, refactored) + + def test_not_renaming_bytes_string_prefix(self): + code = dedent("""\ + b = 1 + print(b"x", rb"y", b) + """) + expected = dedent("""\ + new_b = 1 + print(b"x", rb"y", new_b) + """) + refactored = self._local_rename(code, 0, "new_b") + 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"