Skip to content

fix: escape filename in markdown report #2141#2142

Merged
nedbat merged 6 commits intocoveragepy:mainfrom
ellieayla:fix-issue-2141-markdown-escape
Mar 8, 2026
Merged

fix: escape filename in markdown report #2141#2142
nedbat merged 6 commits intocoveragepy:mainfrom
ellieayla:fix-issue-2141-markdown-escape

Conversation

@ellieayla
Copy link
Copy Markdown
Contributor

Both underscore and pipe characters are valid in filenames and meaningful in markdown tables. Underscores were already escaped by the markdown report, using a simple string replacement, but pipe characters were not.

touch 'pipe|.py'
uv tool run dist/coverage-7.13.5a0.dev1-cp314-cp314-macosx_11_0_arm64.whl erase
uv tool run dist/coverage-7.13.5a0.dev1-cp314-cp314-macosx_11_0_arm64.whl run 'pipe|.py'
uv tool run dist/coverage-7.13.5a0.dev1-cp314-cp314-macosx_11_0_arm64.whl report --format=markdown
| Name      |    Stmts |     Miss |    Cover |
|---------- | -------: | -------: | -------: |
| pipe\|.py |        0 |        0 |     100% |
| **TOTAL** |    **0** |    **0** | **100%** |

aka

Name Stmts Miss Cover
pipe|.py 0 0 100%
TOTAL 0 0 100%

…2141

Both underscore and pipe characters are valid in filenames and meaningful in
markdown tables. Underscores were already escaped by the markdown report,
using a simple string replacement, but pipe characters were not.
@ellieayla ellieayla force-pushed the fix-issue-2141-markdown-escape branch from c7162e4 to b014e0e Compare March 7, 2026 22:36
@ellieayla
Copy link
Copy Markdown
Contributor Author

ruff format --check was mad about 'single quotes' and a wrapped function definition. Force-pushed with its recommendations included.

@nedbat
Copy link
Copy Markdown
Member

nedbat commented Mar 7, 2026

Thanks! I think we need to escape more than just underscore and pipe. I'm surprised this hasn't come up already: don't we need to escape backslashes? If a file name starts with pipe, on Windows, it would be foo\|pipe.py which would be escaped as foo\\|pipe.py, the two backslashes become one, and the pipe isn't actually escaped. File names can also contain stars, or be valid markdown urls: hello[x](y).py. Those characters need escaping.

Since Commonmark says all ASCII punctuation can be escaped, we can do all of string.punctuation. That will require adjusting the tests. The get_report() helper function already knows the output_format, so it can do backslash-aware converting of backslashes to forward slashes for the OS-insensitivity we need in the tests.

@ellieayla
Copy link
Copy Markdown
Contributor Author

ellieayla commented Mar 8, 2026

If the whole of string.punctuation is needed, then a chain of s.replace() becomes super unwieldy.

Instead, how about str.maketrans + str.translate?

edit: 4 tests are affected. But since .,/- aren't meaningful, leave those characters out and avoid changing the common cases - then 0 of the existing tests fail (on mac)

diff --git a/coverage/report.py b/coverage/report.py
index 544106f3..bf6a7982 100644
--- a/coverage/report.py
+++ b/coverage/report.py
@@ -7,6 +7,7 @@
 
 import sys
 from collections.abc import Iterable
+from string import punctuation
 from typing import IO, TYPE_CHECKING, Any
 
 from coverage.exceptions import ConfigError, NoDataError
@@ -19,6 +20,13 @@
 if TYPE_CHECKING:
     from coverage import Coverage
 
+ESCAPE_PUNCTUATION_TABLE = "".maketrans(
+    {char: f"\\{char}" for char in punctuation if char not in ".,/-"}
+)
+
+
+def escape_markdown(input_value: str) -> str:
+    """Prefix all characters meaningful in markdown tables with backslashes."""
+    return input_value.translate(ESCAPE_PUNCTUATION_TABLE)
+
 
 class SummaryReporter:
     """A reporter for writing the summary report."""
@@ -127,10 +135,6 @@ def report_markdown(
 
         """
 
-        def escape_markdown(input_value: str) -> str:
-            """Crudely replace characters meaningful in markdown tables."""
-            return input_value.replace("_", "\\_").replace("|", "\\|")
-
         # Prepare the formatting strings, header, and column sorting.
         max_name = max((len(escape_markdown(line[0])) for line in lines_values), default=0)
         max_name = max(max_name, len("**TOTAL**")) + 1

@nedbat
Copy link
Copy Markdown
Member

nedbat commented Mar 8, 2026

TBH, this comment (not yours!) kind of horrified me:

# get_report() escapes backslash so we expect forward slash escaped underscore

That slipped past me in a previous pull request. It seems like a hacky way to deal with the windows backslashes.

I would amend the get_report() helper function to have an explicit output_format argument, and then:

-        cov.report(file=repout, **kwargs)
-        report = repout.getvalue().replace("\\", "/")
+        if output_format == "markdown":
+            report = report.replace("\\\\", r"\/")
+        else:
+            report = report.replace("\\", "/")

@ellieayla
Copy link
Copy Markdown
Contributor Author

Is SummaryTest.test_empty_files really the only test that failed on Windows at commit 5967aab? Or just the first & fast-failed?

@ellieayla ellieayla closed this Mar 8, 2026
@ellieayla ellieayla reopened this Mar 8, 2026
@ellieayla
Copy link
Copy Markdown
Contributor Author

I added a parametrized test test_markdown_escape_filename to cover all the punctuation, with carveouts for Windows.

I think this is ready to review.

There's several separate commits for various pieces of feedback. Do you want to squash (some of) them?

@ellieayla ellieayla changed the title fix: escape pipe character in filename in markdown report #2141 fix: escape filename in markdown report #2141 Mar 8, 2026
@ellieayla ellieayla requested a review from nedbat March 8, 2026 04:26
@nedbat
Copy link
Copy Markdown
Member

nedbat commented Mar 8, 2026

Thanks, this looks good. I'll squash the commits and update the changelog.

@nedbat nedbat merged commit e06eb34 into coveragepy:main Mar 8, 2026
45 checks passed
nedbat added a commit that referenced this pull request Mar 8, 2026
@nedbat
Copy link
Copy Markdown
Member

nedbat commented Mar 8, 2026

This is finished in commit e06eb34, and tweaked just a tad in fe9d227.

@ellieayla ellieayla deleted the fix-issue-2141-markdown-escape branch March 9, 2026 00:48
@nedbat
Copy link
Copy Markdown
Member

nedbat commented Mar 17, 2026

This is now released as part of coverage 7.13.5.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants