diff --git a/coverage/phystokens.py b/coverage/phystokens.py index 9fc36ecda..0c344b0af 100644 --- a/coverage/phystokens.py +++ b/coverage/phystokens.py @@ -57,10 +57,11 @@ def _phys_tokens(toks: TokenInfos) -> TokenInfos: if last_ttext.endswith("\\"): inject_backslash = False elif ttype == token.STRING: - if last_line.endswith(last_ttext + "\\\n"): + if (last_line.endswith("\\\n") and + last_line.rstrip(" \\\n").endswith(last_ttext)): # Deal with special cases like such code:: # - # a = ["aaa",\ + # a = ["aaa",\ # there may be zero or more blanks between "," and "\". # "bbb \ # ccc"] # @@ -69,6 +70,9 @@ def _phys_tokens(toks: TokenInfos) -> TokenInfos: # It's a multi-line string and the first line ends with # a backslash, so we don't need to inject another. inject_backslash = False + elif sys.version_info >= (3, 12) and ttype == token.FSTRING_MIDDLE: + if ttext.split("\n", 1)[0][-1] == "\\": + inject_backslash = False if inject_backslash: # Figure out what column the backslash is in. ccol = len(last_line.split("\n")[-2]) - 1 diff --git a/tests/test_html.py b/tests/test_html.py index d5cc0f301..5b5e3b86c 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1203,6 +1203,36 @@ def test_bug_1828(self) -> None: '3 ccc"]', ] + @pytest.mark.parametrize( + "leader", ["", "f", "r", "fr", "rf"], + ids=["string", "f-string", "raw_string", "f-raw_string", "raw_f-string"] + ) + def test_bug_1836(self, leader: str) -> None: + # https://github.com/nedbat/coveragepy/issues/1836 + self.make_file("py312_fstrings.py", f"""\ + prog_name = 'bug.py' + err_msg = {leader}'''\\ + {{prog_name}}: ERROR: This is the first line of the error. + {{prog_name}}: ERROR: This is the second line of the error. + \\ + {{prog_name}}: ERROR: This is the third line of the error. + ''' + """) + + cov = coverage.Coverage() + py312_fstrings = self.start_import_stop(cov, "py312_fstrings") + cov.html_report(py312_fstrings) + + assert self.get_html_report_text_lines("py312_fstrings.py") == [ + "1" + "prog_name = 'bug.py'", + "2" + f"err_msg = {leader}'''\\", + "3" + "{prog_name}: ERROR: This is the first line of the error.", + "4" + "{prog_name}: ERROR: This is the second line of the error.", + "5" + "\\", + "6" + "{prog_name}: ERROR: This is the third line of the error.", + "7" + "'''", + ] + def test_unicode(self) -> None: surrogate = "\U000e0100"