diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index cec49d559..7fd60783c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -69,15 +69,13 @@ jobs: python-version: "pypy-3.9" - os: windows python-version: "pypy-3.10" - # GitHub is rolling out macos 14, but it doesn't have Python 3.8 or 3.9. - # https://mastodon.social/@hugovk/112320493602782374 - include: - - python-version: "3.8" - os: "macos" - os-version: "13" - - python-version: "3.9" - os: "macos" - os-version: "13" + # If we need to tweak the os version we can do it with an include like + # this: + # include: + # - python-version: "3.8" + # os: "macos" + # os-version: "13" + # If one job fails, stop the whole thing. fail-fast: true diff --git a/.github/workflows/python-nightly.yml b/.github/workflows/python-nightly.yml index 13c347350..c2b38953d 100644 --- a/.github/workflows/python-nightly.yml +++ b/.github/workflows/python-nightly.yml @@ -31,22 +31,26 @@ concurrency: jobs: tests: - name: "${{ matrix.python-version }}" - # Choose a recent Ubuntu that deadsnakes still builds all the versions for. - # For example, deadsnakes doesn't provide 3.10 nightly for 22.04 (jammy) - # because jammy ships 3.10, and deadsnakes doesn't want to clobber it. - # https://launchpad.net/~deadsnakes/+archive/ubuntu/nightly/+packages - # https://github.com/deadsnakes/issues/issues/234 - # See https://github.com/deadsnakes/nightly for the source of the nightly - # builds. - # bionic: 18, focal: 20, jammy: 22, noble: 24 - runs-on: ubuntu-22.04 + name: "${{ matrix.python-version }} on ${{ matrix.os-short }}" + runs-on: "${{ matrix.os }}" # If it doesn't finish in an hour, it's not going to. Don't spin for six # hours needlessly. timeout-minutes: 60 strategy: matrix: + os: + # Choose a recent Ubuntu that deadsnakes still builds all the versions for. + # For example, deadsnakes doesn't provide 3.10 nightly for 22.04 (jammy) + # because jammy ships 3.10, and deadsnakes doesn't want to clobber it. + # https://launchpad.net/~deadsnakes/+archive/ubuntu/nightly/+packages + # https://github.com/deadsnakes/issues/issues/234 + # See https://github.com/deadsnakes/nightly for the source of the nightly + # builds. + # bionic: 18, focal: 20, jammy: 22, noble: 24 + - "ubuntu-22.04" + os-short: + - "ubuntu" python-version: # When changing this list, be sure to check the [gh] list in # tox.ini so that tox will run properly. PYVERSIONS @@ -58,6 +62,10 @@ jobs: - "pypy-3.8-nightly" - "pypy-3.9-nightly" - "pypy-3.10-nightly" + include: + - python-version: "pypy-3.10-nightly" + os: "windows-latest" + os-short: "windows" fail-fast: false steps: diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index e11b3d74e..9a0f8a74a 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -57,20 +57,18 @@ jobs: exclude: # Windows pypy 3.9 and 3.10 get stuck with PyPy 7.3.15. I hope to # unstick them, but I don't want that to block all other progress, so - # skip them for now. + # skip them for now. These excludes can be removed once GitHub uses + # PyPy 7.3.16 on Windows. https://github.com/pypy/pypy/issues/4876 - os: windows python-version: "pypy-3.9" - os: windows python-version: "pypy-3.10" - # GitHub is rolling out macos 14, but it doesn't have Python 3.8 or 3.9. - # https://mastodon.social/@hugovk/112320493602782374 - include: - - python-version: "3.8" - os: "macos" - os-version: "13" - - python-version: "3.9" - os: "macos" - os-version: "13" + # If we need to tweak the os version we can do it with an include like + # this: + # include: + # - python-version: "3.8" + # os: "macos" + # os-version: "13" fail-fast: false diff --git a/CHANGES.rst b/CHANGES.rst index 9aad1decf..98f263de7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,6 +22,42 @@ upgrading your version of coverage.py. .. scriv-start-here +.. _changes_7-5-1: + +Version 7.5.1 — 2024-05-04 +-------------------------- + +- Fix: a pragma comment on the continuation lines of a multi-line statement + now excludes the statement and its body, the same as if the pragma is + on the first line. This closes `issue 754`_. The fix was contributed by + `Daniel Diniz `_. + +- Fix: very complex source files like `this one `_ could + cause a maximum recursion error when creating an HTML report. This is now + fixed, closing `issue 1774`_. + +- HTML report improvements: + + - Support files (JavaScript and CSS) referenced by the HTML report now have + hashes added to their names to ensure updated files are used instead of + stale cached copies. + + - Missing branch coverage explanations that said "the condition was never + false" now read "the condition was always true" because it's easier to + understand. + + - Column sort order is remembered better as you move between the index pages, + fixing `issue 1766`_. Thanks, `Daniel Diniz `_. + + +.. _resolvent_lookup: https://github.com/sympy/sympy/blob/130950f3e6b3f97fcc17f4599ac08f70fdd2e9d4/sympy/polys/numberfields/resolvent_lookup.py +.. _issue 754: https://github.com/nedbat/coveragepy/issues/754 +.. _issue 1766: https://github.com/nedbat/coveragepy/issues/1766 +.. _pull 1768: https://github.com/nedbat/coveragepy/pull/1768 +.. _pull 1773: https://github.com/nedbat/coveragepy/pull/1773 +.. _issue 1774: https://github.com/nedbat/coveragepy/issues/1774 + + .. _changes_7-5-0: Version 7.5.0 — 2024-04-23 @@ -31,6 +67,7 @@ Version 7.5.0 — 2024-04-23 There are now three index pages which link to each other: files, functions, and classes. Other reports don't yet have this information, but it will be added in the future where it makes sense. Feedback gladly accepted! + Finishes `issue 780`_. - Other HTML report improvements: @@ -50,6 +87,7 @@ Version 7.5.0 — 2024-04-23 - Python 3.13.0a6 is supported. +.. _issue 780: https://github.com/nedbat/coveragepy/issues/780 .. _issue 1384: https://github.com/nedbat/coveragepy/issues/1384 .. _issue 1765: https://github.com/nedbat/coveragepy/issues/1765 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 1a671fac6..809ad01a7 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -63,6 +63,7 @@ Dan Hemberger Dan Riti Dan Wandschneider Danek Duvall +Daniel Diniz Daniel Hahler Danny Allen David Christian diff --git a/Makefile b/Makefile index 12f8f0e96..94943c1f5 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ ##@ Utilities -.PHONY: help clean_platform clean sterile +.PHONY: help clean_platform clean sterile install help: ## Show this help. @# Adapted from https://www.thapaliya.com/en/writings/well-documented-makefiles/ @@ -50,6 +50,9 @@ sterile: clean ## Remove all non-controlled content, even if expensive. rm -rf .tox rm -f cheats.txt +install: ## Install the developer tools + python3 -m pip install -r requirements/dev.pip + ##@ Tests and quality checks @@ -73,10 +76,10 @@ metacov: ## Run meta-coverage, measuring ourself. COVERAGE_COVERAGE=yes tox -q $(ARGS) metahtml: ## Produce meta-coverage HTML reports. - python igor.py combine_html + tox exec -q $(ARGS) -- python3 igor.py combine_html metasmoke: - COVERAGE_TEST_CORES=ctrace ARGS="-e py39" make metacov metahtml + COVERAGE_TEST_CORES=ctrace ARGS="-e py39" make --keep-going metacov metahtml ##@ Requirements management @@ -192,6 +195,7 @@ relcommit1: #: Commit the first release changes (see howto.txt). git commit -am "docs: prep for $$(python setup.py --version)" relcommit2: #: Commit the latest sample HTML report (see howto.txt). + git add doc/sample_html git commit -am "docs: sample HTML for $$(python setup.py --version)" kit: ## Make the source distribution. diff --git a/coverage/files.py b/coverage/files.py index 5fb704350..b2f69a20b 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -87,8 +87,6 @@ def canonical_filename(filename: str) -> str: return CANONICAL_FILENAME_CACHE[filename] -MAX_FLAT = 100 - def flat_rootname(filename: str) -> str: """A base for a flat file name to correspond to this file. diff --git a/coverage/html.py b/coverage/html.py index f32ca0a29..fcb5ab5ed 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -12,7 +12,6 @@ import json import os import re -import shutil import string from dataclasses import dataclass, field @@ -221,7 +220,6 @@ class HtmlReporter: "style.css", "coverage_html.js", "keybd_closed.png", - "keybd_open.png", "favicon_32.png", ] @@ -239,11 +237,7 @@ def __init__(self, cov: Coverage) -> None: title = self.config.html_title - self.extra_css: str | None - if self.config.extra_css: - self.extra_css = os.path.basename(self.config.extra_css) - else: - self.extra_css = None + self.extra_css = bool(self.config.extra_css) self.data = self.coverage.get_data() self.has_arcs = self.data.has_arcs() @@ -271,6 +265,7 @@ def __init__(self, cov: Coverage) -> None: "extra_css": self.extra_css, "has_arcs": self.has_arcs, "show_contexts": self.config.show_contexts, + "statics": {}, # Constants for all reports. # These css classes determine which lines are highlighted by default. @@ -324,6 +319,9 @@ def report(self, morfs: Iterable[TMorf] | None) -> float: if not have_data: raise NoDataError("No data to report.") + self.make_directory() + self.make_local_static_report_files() + if files_to_report: for ftr1, ftr2 in zip(files_to_report[:-1], files_to_report[1:]): ftr1.next_html = ftr2.html_filename @@ -348,7 +346,6 @@ def report(self, morfs: Iterable[TMorf] | None) -> float: # Write function and class index pages. self.write_region_index_pages(files_to_report) - self.make_local_static_report_files() return ( self.index_pages["file"].totals.n_statements and self.index_pages["file"].totals.pc_covered @@ -360,24 +357,40 @@ def make_directory(self) -> None: if not os.listdir(self.directory): self.directory_was_empty = True + def copy_static_file(self, src: str, slug: str = "") -> None: + """Copy a static file into the output directory with cache busting.""" + with open(src, "rb") as f: + text = f.read() + h = Hasher() + h.update(text) + cache_bust = h.hexdigest()[:8] + src_base = os.path.basename(src) + dest = src_base.replace(".", f"_cb_{cache_bust}.") + if not slug: + slug = src_base.replace(".", "_") + self.template_globals["statics"][slug] = dest # type: ignore + with open(os.path.join(self.directory, dest), "wb") as f: + f.write(text) + def make_local_static_report_files(self) -> None: """Make local instances of static files for HTML report.""" + # The files we provide must always be copied. for static in self.STATIC_FILES: - shutil.copyfile(data_filename(static), os.path.join(self.directory, static)) + self.copy_static_file(data_filename(static)) + + # The user may have extra CSS they want copied. + if self.extra_css: + assert self.config.extra_css is not None + self.copy_static_file(self.config.extra_css, slug="extra_css") # Only write the .gitignore file if the directory was originally empty. - # .gitignore can't be copied from the source tree because it would - # prevent the static files from being checked in. + # .gitignore can't be copied from the source tree because if it was in + # the source tree, it would stop the static files from being checked in. if self.directory_was_empty: with open(os.path.join(self.directory, ".gitignore"), "w") as fgi: fgi.write("# Created by coverage.py\n*\n") - # The user may have extra CSS they want copied. - if self.extra_css: - assert self.config.extra_css is not None - shutil.copyfile(self.config.extra_css, os.path.join(self.directory, self.extra_css)) - def should_report(self, analysis: Analysis, index_page: IndexPage) -> bool: """Determine if we'll report this file or region.""" # Get the numbers for this file. @@ -408,8 +421,6 @@ def write_html_page(self, ftr: FileToReport) -> None: only does page summary bookkeeping. """ - self.make_directory() - # Find out if the page on disk is already correct. if self.incr.can_skip_file(self.data, ftr.fr, ftr.rootname): self.index_pages["file"].summaries.append(self.incr.index_info(ftr.rootname)) @@ -502,8 +513,6 @@ def write_html_page(self, ftr: FileToReport) -> None: def write_file_index_page(self, first_html: str, final_html: str) -> None: """Write the file index page for this report.""" - self.make_directory() - index_file = self.write_index_page( self.index_pages["file"], first_html=first_html, diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js index a28c1bef8..0a859a537 100644 --- a/coverage/htmlfiles/coverage_html.js +++ b/coverage/htmlfiles/coverage_html.js @@ -81,7 +81,32 @@ function sortColumn(th) { .forEach(tr => tr.parentElement.appendChild(tr)); // Save the sort order for next time. - localStorage.setItem(coverage.INDEX_SORT_STORAGE, JSON.stringify({column, direction})); + if (th.id !== "region") { + let th_id = "file"; // Sort by file if we don't have a column id + let current_direction = direction; + const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); + if (stored_list) { + ({th_id, direction} = JSON.parse(stored_list)) + } + localStorage.setItem(coverage.INDEX_SORT_STORAGE, JSON.stringify({ + "th_id": th.id, + "direction": current_direction + })); + if (th.id !== th_id || document.getElementById("region")) { + // Sort column has changed, unset sorting by function or class. + localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({ + "by_region": false, + "region_direction": current_direction + })); + } + } + else { + // Sort column has changed to by function or class, remember that. + localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({ + "by_region": true, + "region_direction": direction + })); + } } // Find all the elements with data-shortcut attribute, and use them to assign a shortcut key. @@ -223,18 +248,39 @@ coverage.wire_up_sorting = function () { ); // Look for a localStorage item containing previous sort settings: - var column = 0, direction = "ascending"; + let th_id = "file", direction = "ascending"; const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); if (stored_list) { - ({column, direction} = JSON.parse(stored_list)); + ({th_id, direction} = JSON.parse(stored_list)); + } + let by_region = false, region_direction = "ascending"; + const sorted_by_region = localStorage.getItem(coverage.SORTED_BY_REGION); + if (sorted_by_region) { + ({ + by_region, + region_direction + } = JSON.parse(sorted_by_region)); } - const th = document.querySelector("[data-sortable]").tHead.rows[0].cells[column]; // nosemgrep: eslint.detect-object-injection + const region_id = "region"; + if (by_region && document.getElementById(region_id)) { + direction = region_direction; + } + // If we are in a page that has a column with id of "region", sort on + // it if the last sort was by function or class. + let th; + if (document.getElementById(region_id)) { + th = document.getElementById(by_region ? region_id : th_id); + } + else { + th = document.getElementById(th_id); + } th.setAttribute("aria-sort", direction === "ascending" ? "descending" : "ascending"); th.click() }; coverage.INDEX_SORT_STORAGE = "COVERAGE_INDEX_SORT_2"; +coverage.SORTED_BY_REGION = "COVERAGE_SORT_REGION"; // Loaded on index.html coverage.index_ready = function () { diff --git a/coverage/htmlfiles/index.html b/coverage/htmlfiles/index.html index 69d4b19ed..f75d18b43 100644 --- a/coverage/htmlfiles/index.html +++ b/coverage/htmlfiles/index.html @@ -6,12 +6,12 @@ {{ title|escape }} - - + + {% if extra_css %} - + {% endif %} - + @@ -24,7 +24,7 @@

{{ title|escape }}: