diff --git a/CHANGELOG.md b/CHANGELOG.md index 1650aa77a..bbe9ba26f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - #623, #819, #863 Support MatchOr, MatchSequence, MatchStar (@jheld, @lieryan) - #870 Add default implementation for is_dir() (@lieryan) - #872 Fix unicode handling in patchedast (@lieryan) +- Fix patchedast end-quote detection for `'''` inside a double-quoted f-string # Release 1.14.0 diff --git a/rope/refactor/patchedast.py b/rope/refactor/patchedast.py index 2a52fc924..8df52f088 100644 --- a/rope/refactor/patchedast.py +++ b/rope/refactor/patchedast.py @@ -385,14 +385,19 @@ def start_quote_char(): return self.source[start : quote_pos + len(quote_char)] def end_quote_char(): - possible_quotes = [ - (self.source.source.rfind(q, start, end), q) - for q in reversed(QUOTE_CHARS) - ] - _, quote_pos, quote_char = max( - (len(q), pos, q) for pos, q in possible_quotes if pos != -1 + # The closing delimiter is the quote char the f-string literally + # ends with -- NOT the longest quote char anywhere in the body. + # A ''' appearing inside a "..." f-string body must not be picked + # as the delimiter (issue: end_quote_char used max(len(q)) and + # mis-matched, producing a token that consume_joined_string could + # not locate). + for quote_char in QUOTE_CHARS: + if self.source.source[end - len(quote_char) : end] == quote_char: + return self.source[end - len(quote_char) : end] + raise MismatchedTokenError( + f"f-string at line {self.lines.get_line_number(start)} does not" + " end with a quote delimiter" ) - return self.source[end - len(quote_char) : end] QUOTE_CHARS = ['"""', "'''", '"', "'"] offset = self.source.offset diff --git a/ropetest/refactor/patchedasttest.py b/ropetest/refactor/patchedasttest.py index c014f6ed1..c50454035 100644 --- a/ropetest/refactor/patchedasttest.py +++ b/ropetest/refactor/patchedasttest.py @@ -326,6 +326,19 @@ def test_handling_format_strings_basic(self): checker.check_children("JoinedStr", ['f"', "abc", "FormattedValue", "", '"']) checker.check_children("FormattedValue", ["{", "", "Name", "", "}"]) + @testutils.only_for_versions_higher("3.6") + def test_handling_format_strings_with_triple_quote_in_body(self): + # A double-quoted f-string whose body contains a ''' sequence must not + # confuse the end-quote detection (end_quote_char picked the longest + # quote in the body instead of the matching delimiter). + source = 'f"abc = \'\'\'{a}"\n' + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + checker.check_children( + "JoinedStr", ['f"', "abc = '''", "FormattedValue", "", '"'] + ) + checker.check_children("FormattedValue", ["{", "", "Name", "", "}"]) + @testutils.only_for_versions_higher("3.6") def test_handling_format_strings_with_implicit_join(self): source = '''"1" + rf'abc{a}' f"""xxx{b} """\n'''