coveragepy icon indicating copy to clipboard operation
coveragepy copied to clipboard

Problem with html rendering of f-string with double curly brackets

Open mathieugouin opened this issue 5 months ago • 5 comments

Describe the bug Generated html coverage report of file with f-string with double curly brackets like: f"{{this is not interpolated}}" is wrongly rendered by removing one set of bracket.

To Reproduce

  • What version of Python are you using? Python 3.13.3
  • What version of coverage.py shows the problem? Coverage.py, version 7.8.2 with C extension
coverage debug sys


-- sys -------------------------------------------------------
               coverage_version: 7.8.2
                coverage_module: D:\Temp\python_coverage\.venv\Lib\site-packages\coverage\__init__.py
                           core: -none-
                        CTracer: available from D:\Temp\python_coverage\.venv\Lib\site-packages\coverage\tracer.cp313-win_amd64.pyd
           plugins.file_tracers: -none-
            plugins.configurers: -none-
      plugins.context_switchers: -none-
              configs_attempted: D:\Temp\python_coverage\.coveragerc
                                 D:\Temp\python_coverage\setup.cfg
                                 D:\Temp\python_coverage\tox.ini
                                 D:\Temp\python_coverage\pyproject.toml
                   configs_read: -none-
                    config_file: None
                config_contents: -none-
                      data_file: -none-
                         python: 3.13.3 (tags/v3.13.3:6280bb5, Apr  8 2025, 14:47:33) [MSC v.1943 64 bit (AMD64)]
                       platform: Windows-10-10.0.19045-SP0
                 implementation: CPython
                    gil_enabled: True
                     executable: D:\Temp\python_coverage\.venv\Scripts\python.exe
                   def_encoding: utf-8
                    fs_encoding: utf-8
                            pid: 6600
                            cwd: D:\Temp\python_coverage
                           path: D:\Temp\python_coverage\.venv\Scripts\coverage.exe
                                 C:\Python313\python313.zip
                                 C:\Python313\DLLs
                                 C:\Python313\Lib
                                 C:\Python313
                                 D:\Temp\python_coverage\.venv
                                 D:\Temp\python_coverage\.venv\Lib\site-packages
                    environment: TEMP = C:\Users\mgouin\AppData\Local\Temp
                                 TMP = C:\Users\mgouin\AppData\Local\Temp
                   command_line: D:\Temp\python_coverage\.venv\Scripts\coverage debug sys
         sqlite3_sqlite_version: 3.49.1
             sqlite3_temp_store: 0
        sqlite3_compile_options: ATOMIC_INTRINSICS=0, COMPILER=msvc-1943, DEFAULT_AUTOVACUUM,
                                 DEFAULT_CACHE_SIZE=-2000, DEFAULT_FILE_FORMAT=4,
                                 DEFAULT_JOURNAL_SIZE_LIMIT=-1, DEFAULT_MMAP_SIZE=0, DEFAULT_PAGE_SIZE=4096,
                                 DEFAULT_PCACHE_INITSZ=20, DEFAULT_RECURSIVE_TRIGGERS,
                                 DEFAULT_SECTOR_SIZE=4096, DEFAULT_SYNCHRONOUS=2,
                                 DEFAULT_WAL_AUTOCHECKPOINT=1000, DEFAULT_WAL_SYNCHRONOUS=2,
                                 DEFAULT_WORKER_THREADS=0, DIRECT_OVERFLOW_READ, ENABLE_FTS3, ENABLE_FTS4,
                                 ENABLE_FTS5, ENABLE_MATH_FUNCTIONS, ENABLE_RTREE, MALLOC_SOFT_LIMIT=1024,
                                 MAX_ATTACHED=10, MAX_COLUMN=2000, MAX_COMPOUND_SELECT=500,
                                 MAX_DEFAULT_PAGE_SIZE=8192, MAX_EXPR_DEPTH=1000, MAX_FUNCTION_ARG=1000,
                                 MAX_LENGTH=1000000000, MAX_LIKE_PATTERN_LENGTH=50000,
                                 MAX_MMAP_SIZE=0x7fff0000, MAX_PAGE_COUNT=0xfffffffe, MAX_PAGE_SIZE=65536,
                                 MAX_SQL_LENGTH=1000000000, MAX_TRIGGER_DEPTH=1000,
                                 MAX_VARIABLE_NUMBER=32766, MAX_VDBE_OP=250000000, MAX_WORKER_THREADS=8,
                                 MUTEX_W32, OMIT_AUTOINIT, SYSTEM_MALLOC, TEMP_STORE=1, THREADSAFE=1
  • What versions of what packages do you have installed? The output of pip freeze is helpful.
pip freeze
coverage==7.8.2
  • What code shows the problem?

Simple file: main.py :

i = 54
print(f"{{this is not an interpolated variable}} but this is: i = {i}")
  • What commands should we run to reproduce the problem? Be specific. Include everything, even git clone, pip install, and so on. Explain like we're five!

Create the main.py file with content above.

Generate coverage:

coverage run main.py

Generate html report:

coverage html

Open the html report in htmlcov/main_py.html

You see that the report is not showing the correct content of the print line.

Image

In plain text:

print(f"{ this is not an interpolated variable}  but this is: i = {i}") 

Expected behavior

The html rendered line should match exactly the source code which should be:

print(f"{{this is not an interpolated variable}} but this is: i = {i}")

Additional context n/a

mathieugouin avatar Jun 05 '25 16:06 mathieugouin

Hmm, I don't see the same behavior. I tried with 3.13. Then I tried running under 3.13 and generating the html with 3.9, then I did the reverse. In all cases the HTML looked correct:

Image

Something else must be different. Can you attach the HTML file with the problem?

nedbat avatar Jun 05 '25 22:06 nedbat

Apparently, I cannot attach html files:

Image

However, I looked at it myself and it is clear the problem:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Coverage for main.py: 100%</title>
    <link rel="icon" sizes="32x32" href="favicon_32_cb_58284776.png">
    <link rel="stylesheet" href="style_cb_718ce007.css" type="text/css">
    <script src="coverage_html_cb_497bf287.js" defer></script>
</head>
<body class="pyfile">
<header>
    <div class="content">
        <h1>
            <span class="text">Coverage for </span><b>main.py</b>:
            <span class="pc_cov">100%</span>
        </h1>
        <aside id="help_panel_wrapper">
            <input id="help_panel_state" type="checkbox">
            <label for="help_panel_state">
                <img id="keyboard_icon" src="keybd_closed_cb_ce680311.png" alt="Show/hide keyboard shortcuts">
            </label>
            <div id="help_panel">
                <p class="legend">Shortcuts on this page</p>
                <div class="keyhelp">
                    <p>
                        <kbd>r</kbd>
                        <kbd>m</kbd>
                        <kbd>x</kbd>
                        &nbsp; toggle line displays
                    </p>
                    <p>
                        <kbd>j</kbd>
                        <kbd>k</kbd>
                        &nbsp; next/prev highlighted chunk
                    </p>
                    <p>
                        <kbd>0</kbd> &nbsp; (zero) top of page
                    </p>
                    <p>
                        <kbd>1</kbd> &nbsp; (one) first highlighted chunk
                    </p>
                    <p>
                        <kbd>[</kbd>
                        <kbd>]</kbd>
                        &nbsp; prev/next file
                    </p>
                    <p>
                        <kbd>u</kbd> &nbsp; up to the index
                    </p>
                    <p>
                        <kbd>?</kbd> &nbsp; show/hide this help
                    </p>
                </div>
            </div>
        </aside>
        <h2>
            <span class="text">2 statements &nbsp;</span>
            <button type="button" class="run button_toggle_run" value="run" data-shortcut="r" title="Toggle lines run">2<span class="text"> run</span></button>
            <button type="button" class="mis show_mis button_toggle_mis" value="mis" data-shortcut="m" title="Toggle lines missing">0<span class="text"> missing</span></button>
            <button type="button" class="exc show_exc button_toggle_exc" value="exc" data-shortcut="x" title="Toggle lines excluded">0<span class="text"> excluded</span></button>
        </h2>
        <p class="text">
            <a id="prevFileLink" class="nav" href="index.html">&#xab; prev</a> &nbsp; &nbsp;
            <a id="indexLink" class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
            <a id="nextFileLink" class="nav" href="index.html">&#xbb; next</a>
            &nbsp; &nbsp; &nbsp;
            <a class="nav" href="https://coverage.readthedocs.io/en/7.8.2">coverage.py v7.8.2</a>,
            created at 2025-06-06 10:10 -0400
        </p>
        <aside class="hidden">
            <button type="button" class="button_next_chunk" data-shortcut="j"></button>
            <button type="button" class="button_prev_chunk" data-shortcut="k"></button>
            <button type="button" class="button_top_of_page" data-shortcut="0"></button>
            <button type="button" class="button_first_chunk" data-shortcut="1"></button>
            <button type="button" class="button_prev_file" data-shortcut="["></button>
            <button type="button" class="button_next_file" data-shortcut="]"></button>
            <button type="button" class="button_to_index" data-shortcut="u"></button>
            <button type="button" class="button_show_hide_help" data-shortcut="?"></button>
        </aside>
    </div>
</header>
<main id="source">
    <p class="run"><span class="n"><a id="t1" href="#t1">1</a></span><span class="t"><span class="nam">i</span> <span class="op">=</span> <span class="num">54</span>&nbsp;</span><span class="r"></span></p>
    <p class="run"><span class="n"><a id="t2" href="#t2">2</a></span><span class="t"><span class="nam">print</span><span class="op">(</span><span class="fst">f"</span><span class="fst">{</span> <span class="fst">this is not an interpolated variable}</span> <span class="fst"> but this is: i = </span><span class="op">{</span><span class="nam">i</span><span class="op">}</span><span class="fst">"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
    <p class="pln"><span class="n"><a id="t3" href="#t3">3</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
</main>
<footer>
    <div class="content">
        <p>
            <a class="nav" href="index.html">&#xab; prev</a> &nbsp; &nbsp;
            <a class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
            <a class="nav" href="index.html">&#xbb; next</a>
            &nbsp; &nbsp; &nbsp;
            <a class="nav" href="https://coverage.readthedocs.io/en/7.8.2">coverage.py v7.8.2</a>,
            created at 2025-06-06 10:10 -0400
        </p>
    </div>
</footer>
</body>
</html>

mathieugouin avatar Jun 06 '25 14:06 mathieugouin

The relevant issue:

    <p class="run"><span class="n"><a id="t2" href="#t2">2</a></span><span class="t"><span class="nam">print</span><span class="op">(</span><span class="fst">f"</span><span class="fst">{</span> <span class="fst">this is not an interpolated variable}</span> <span class="fst"> but this is: i = </span><span class="op">{</span><span class="nam">i</span><span class="op">}</span><span class="fst">"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>

See there is an empty space:

                                   | here
-----------------------------------V
f"</span><span class="fst">{</span> <span class="fst">this is not

Also: interpolated variable}</span> <span

mathieugouin avatar Jun 06 '25 14:06 mathieugouin

Hmm, I'm not sure what I did wrong last time. Now I see the same thing you do. This is a change in the behavior of the tokenize module in Python 3.12. I suspect it won't be considered a Python bug, so I'll have to fix it in coverage.

nedbat avatar Jun 06 '25 16:06 nedbat

Ok, thanks for the update!

Mathieu

On Fri., Jun. 6, 2025, 12:11 Ned Batchelder, @.***> wrote:

nedbat left a comment (nedbat/coveragepy#1980) https://github.com/nedbat/coveragepy/issues/1980#issuecomment-2949762379

Hmm, I'm not sure what I did wrong last time. Now I see the same thing you do. This is a change in the behavior of the tokenize module in Python 3.12. I suspect it won't be considered a Python bug, so I'll have to fix it in coverage.

— Reply to this email directly, view it on GitHub https://github.com/nedbat/coveragepy/issues/1980#issuecomment-2949762379, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACYG2PQUCXQJCRIXL67VB5D3CG4RPAVCNFSM6AAAAAB6V33VS2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDSNBZG43DEMZXHE . You are receiving this because you authored the thread.Message ID: @.***>

mathieugouin avatar Jun 07 '25 02:06 mathieugouin

This is fixed in commit 9f94c87677fd86c7ced9ea2f3cddfcc0b4d5bc2d.

nedbat avatar Jun 09 '25 00:06 nedbat

This is now released as part of coverage 7.9.0.

nedbat avatar Jun 11 '25 23:06 nedbat

Thanks !

mathieugouin avatar Jun 11 '25 23:06 mathieugouin