diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3408e4455..c6841b18a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,7 +7,11 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - # Check for updates to GitHub Actions every weekday - interval: "daily" + # Check for updates to GitHub Actions once a week + interval: "weekly" + groups: + action-dependencies: + patterns: + - "*" commit-message: prefix: "chore" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0934c2528..fbb600a5d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -49,7 +49,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3 + uses: github/codeql-action/init@2d790406f505036ef40ecba973cc774a50395aac # v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3 + uses: github/codeql-action/autobuild@2d790406f505036ef40ecba973cc774a50395aac # v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -74,4 +74,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3 + uses: github/codeql-action/analyze@2d790406f505036ef40ecba973cc774a50395aac # v3 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 912e215f5..c578fafec 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: 'Dependency Review' - uses: actions/dependency-review-action@72eb03d02c7872a771aacd928f3123ac62ad6d3a # v4.3.3 + uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 with: base-ref: ${{ github.event.pull_request.base.sha || 'master' }} head-ref: ${{ github.event.pull_request.head.sha || github.ref }} diff --git a/.github/workflows/kit.yml b/.github/workflows/kit.yml index a54bdbc01..d9e485bab 100644 --- a/.github/workflows/kit.yml +++ b/.github/workflows/kit.yml @@ -77,10 +77,9 @@ jobs: # "macos": ["arm64", "x86_64"], # "windows": ["x86", "AMD64"], # } - # # PYVERSIONS. Available versions: - # # https://github.com/actions/python-versions/blob/main/versions-manifest.json + # # PYVERSIONS. Available versions: https://pypi.org/project/cibuildwheel/ # # PyPy versions are handled further below in the "pypy" step. - # pys = ["cp38", "cp39", "cp310", "cp311", "cp312"] + # pys = ["cp38", "cp39", "cp310", "cp311", "cp312", "cp313"] # # # Some OS/arch combinations need overrides for the Python versions: # os_arch_pys = { @@ -107,43 +106,50 @@ jobs: - {"os": "ubuntu", "py": "cp310", "arch": "x86_64"} - {"os": "ubuntu", "py": "cp311", "arch": "x86_64"} - {"os": "ubuntu", "py": "cp312", "arch": "x86_64"} + - {"os": "ubuntu", "py": "cp313", "arch": "x86_64"} - {"os": "ubuntu", "py": "cp38", "arch": "i686"} - {"os": "ubuntu", "py": "cp39", "arch": "i686"} - {"os": "ubuntu", "py": "cp310", "arch": "i686"} - {"os": "ubuntu", "py": "cp311", "arch": "i686"} - {"os": "ubuntu", "py": "cp312", "arch": "i686"} + - {"os": "ubuntu", "py": "cp313", "arch": "i686"} - {"os": "ubuntu", "py": "cp38", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp39", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp310", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp311", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp312", "arch": "aarch64"} + - {"os": "ubuntu", "py": "cp313", "arch": "aarch64"} - {"os": "macos", "py": "cp38", "arch": "arm64", "os-version": "13"} - {"os": "macos", "py": "cp39", "arch": "arm64", "os-version": "13"} - {"os": "macos", "py": "cp310", "arch": "arm64", "os-version": "13"} - {"os": "macos", "py": "cp311", "arch": "arm64", "os-version": "13"} - {"os": "macos", "py": "cp312", "arch": "arm64", "os-version": "13"} + - {"os": "macos", "py": "cp313", "arch": "arm64", "os-version": "13"} - {"os": "macos", "py": "cp38", "arch": "x86_64", "os-version": "13"} - {"os": "macos", "py": "cp39", "arch": "x86_64", "os-version": "13"} - {"os": "macos", "py": "cp310", "arch": "x86_64", "os-version": "13"} - {"os": "macos", "py": "cp311", "arch": "x86_64", "os-version": "13"} - {"os": "macos", "py": "cp312", "arch": "x86_64", "os-version": "13"} + - {"os": "macos", "py": "cp313", "arch": "x86_64", "os-version": "13"} - {"os": "windows", "py": "cp38", "arch": "x86"} - {"os": "windows", "py": "cp39", "arch": "x86"} - {"os": "windows", "py": "cp310", "arch": "x86"} - {"os": "windows", "py": "cp311", "arch": "x86"} - {"os": "windows", "py": "cp312", "arch": "x86"} + - {"os": "windows", "py": "cp313", "arch": "x86"} - {"os": "windows", "py": "cp38", "arch": "AMD64"} - {"os": "windows", "py": "cp39", "arch": "AMD64"} - {"os": "windows", "py": "cp310", "arch": "AMD64"} - {"os": "windows", "py": "cp311", "arch": "AMD64"} - {"os": "windows", "py": "cp312", "arch": "AMD64"} - # [[[end]]] (checksum: 16ed28c185d540b2d9972a0217864472) + - {"os": "windows", "py": "cp313", "arch": "AMD64"} + # [[[end]]] (checksum: e0cd49f4a0028c4fdf1036e9bc843075) fail-fast: false steps: - name: "Setup QEMU" if: matrix.os == 'ubuntu' - uses: docker/setup-qemu-action@5927c834f5b4fdf503fca6f4c7eccda82949e1ee # v3.1.0 + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 with: platforms: arm64 @@ -164,10 +170,11 @@ jobs: - name: "Build wheels" env: - CIBW_BUILD: ${{ matrix.py }}-* + CIBW_BUILD: ${{ matrix.py }}*-* CIBW_ARCHS: ${{ matrix.arch }} CIBW_ENVIRONMENT: PIP_DISABLE_PIP_VERSION_CHECK=1 CIBW_PRERELEASE_PYTHONS: True + CIBW_FREE_THREADED_SUPPORT: True CIBW_TEST_COMMAND: python -c "from coverage.tracer import CTracer; print('CTracer OK!')" run: | python -m cibuildwheel --output-dir wheelhouse @@ -285,8 +292,14 @@ jobs: pattern: dist-* merge-multiple: true + - name: "List distributions" + run: | + ls -alR + echo "Number of dists, there should be 72:" + ls -1 coverage-* | wc -l + - name: "Sign artifacts" - uses: sigstore/gh-action-sigstore-python@61f6a500bbfdd9a2a339cf033e5421951fbc1cd2 # v2.1.1 + uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0 with: inputs: coverage-*.* @@ -299,7 +312,5 @@ jobs: with: name: signatures path: | - *.crt - *.sig - *.sigstore + *.sigstore.json retention-days: 7 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c4d5746d1..35ae00e28 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -68,7 +68,7 @@ jobs: - name: "What did we get?" run: | ls -alR - echo "Number of dists:" + echo "Number of dists, should be 72:" ls -1 dist | wc -l - name: "Generate attestations" @@ -107,7 +107,7 @@ jobs: - name: "What did we get?" run: | ls -alR - echo "Number of dists:" + echo "Number of dists, should be 72:" ls -1 dist | wc -l - name: "Generate attestations" diff --git a/.github/workflows/python-nightly.yml b/.github/workflows/python-nightly.yml index fb4341e27..a91e58c41 100644 --- a/.github/workflows/python-nightly.yml +++ b/.github/workflows/python-nightly.yml @@ -104,6 +104,7 @@ jobs: set -xe python -VV python -m site + python -m sysconfig python -c "import sys; print('GIL:', getattr(sys, '_is_gil_enabled', lambda: True)())" python -m coverage debug sys python -m coverage debug pybehave diff --git a/CHANGES.rst b/CHANGES.rst index 13e3c1eb9..573cbdefe 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,6 +22,24 @@ upgrading your version of coverage.py. .. scriv-start-here +.. _changes_7-6-1: + +Version 7.6.1 — 2024-08-04 +-------------------------- + +- Fix: coverage used to fail when measuring code using :func:`runpy.run_path + ` with a :class:`Path ` argument. + This is now fixed, thanks to `Ask Hjorth Larsen `_. + +- Fix: backslashes preceding a multi-line backslashed string could confuse the + HTML report. This is now fixed, thanks to `LiuYinCarl `_. + +- Now we publish wheels for Python 3.13, both regular and free-threaded. + +.. _pull 1819: https://github.com/nedbat/coveragepy/pull/1819 +.. _pull 1828: https://github.com/nedbat/coveragepy/pull/1828 + + .. _changes_7-6-0: Version 7.6.0 — 2024-07-11 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 728f2fb1d..9097546ea 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -25,6 +25,7 @@ Artem Dayneko Arthur Deygin Arthur Rio Asher Foa +Ask Hjorth Larsen Ben Carlsson Ben Finney Benjamin Parzella @@ -134,6 +135,7 @@ Leonardo Pistone Lewis Gaul Lex Berezhny Liam DeVoe +LiuYinCarl Loïc Dachary Lorenzo Micò Louis Heredero diff --git a/README.rst b/README.rst index 04df255ff..203f8357c 100644 --- a/README.rst +++ b/README.rst @@ -25,7 +25,7 @@ Coverage.py runs on these versions of Python: .. PYVERSIONS -* Python 3.8 through 3.12, and 3.13.0b3. +* Python 3.8 through 3.12, and 3.13.0rc1, including free-threading. * PyPy3 versions 3.8 through 3.10. Documentation is on `Read the Docs`_. Code repository and issue tracker are on diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 9f9c06559..7c01ccbf0 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -19,9 +19,9 @@ import coverage from coverage import Coverage from coverage import env -from coverage.collector import HAS_CTRACER from coverage.config import CoverageConfig from coverage.control import DEFAULT_DATAFILE +from coverage.core import HAS_CTRACER from coverage.data import combinable_files, debug_data_file from coverage.debug import info_header, short_stack, write_formatted_info from coverage.exceptions import _BaseCoverageException, _ExceptionDuringRun, NoSource diff --git a/coverage/collector.py b/coverage/collector.py index 93860e43c..470d3a7e3 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -17,38 +17,27 @@ from coverage import env from coverage.config import CoverageConfig +from coverage.core import Core from coverage.data import CoverageData from coverage.debug import short_stack -from coverage.disposition import FileDisposition from coverage.exceptions import ConfigError from coverage.misc import human_sorted_items, isolate_module from coverage.plugin import CoveragePlugin -from coverage.pytracer import PyTracer -from coverage.sysmon import SysMonitor from coverage.types import ( - TArc, TFileDisposition, TTraceData, TTraceFn, TracerCore, TWarnFn, + TArc, + TCheckIncludeFn, + TFileDisposition, + TShouldStartContextFn, + TShouldTraceFn, + TTraceData, + TTraceFn, + Tracer, + TWarnFn, ) os = isolate_module(os) -try: - # Use the C extension code when we can, for speed. - from coverage.tracer import CTracer, CFileDisposition - HAS_CTRACER = True -except ImportError: - # Couldn't import the C extension, maybe it isn't built. - if os.getenv("COVERAGE_CORE") == "ctrace": # pragma: part covered - # During testing, we use the COVERAGE_CORE environment variable - # to indicate that we've fiddled with the environment to test this - # fallback code. If we thought we had a C tracer, but couldn't import - # it, then exit quickly and clearly instead of dribbling confusing - # errors. I'm using sys.exit here instead of an exception because an - # exception here causes all sorts of other noise in unittest. - sys.stderr.write("*** COVERAGE_CORE is 'ctrace' but can't import CTracer!\n") - sys.exit(1) - HAS_CTRACER = False - T = TypeVar("T") @@ -78,15 +67,14 @@ class Collector: def __init__( self, - should_trace: Callable[[str, FrameType], TFileDisposition], - check_include: Callable[[str, FrameType], bool], - should_start_context: Callable[[FrameType], str | None] | None, + core: Core, + should_trace: TShouldTraceFn, + check_include: TCheckIncludeFn, + should_start_context: TShouldStartContextFn | None, file_mapper: Callable[[str], str], - timid: bool, branch: bool, warn: TWarnFn, concurrency: list[str], - metacov: bool, ) -> None: """Create a collector. @@ -105,11 +93,6 @@ def __init__( filename. The result is the name that will be recorded in the data file. - If `timid` is true, then a slower simpler trace function will be - used. This is important for some environments where manipulation of - tracing functions make the faster more sophisticated trace function not - operate properly. - If `branch` is true, then branches will be measured. This involves collecting data on which statements followed each other (arcs). Use `get_arc_data` to get the arc data. @@ -124,6 +107,7 @@ def __init__( Other values are ignored. """ + self.core = core self.should_trace = should_trace self.check_include = check_include self.should_start_context = should_start_context @@ -143,52 +127,6 @@ def __init__( self.concur_id_func = None - self._trace_class: type[TracerCore] - self.file_disposition_class: type[TFileDisposition] - - core: str | None - if timid: - core = "pytrace" - else: - core = os.getenv("COVERAGE_CORE") - - if core == "sysmon" and not env.PYBEHAVIOR.pep669: - self.warn("sys.monitoring isn't available, using default core", slug="no-sysmon") - core = None - - if not core: - # Once we're comfortable with sysmon as a default: - # if env.PYBEHAVIOR.pep669 and self.should_start_context is None: - # core = "sysmon" - if HAS_CTRACER: - core = "ctrace" - else: - core = "pytrace" - - if core == "sysmon": - self._trace_class = SysMonitor - self._core_kwargs = {"tool_id": 3 if metacov else 1} - self.file_disposition_class = FileDisposition - self.supports_plugins = False - self.packed_arcs = False - self.systrace = False - elif core == "ctrace": - self._trace_class = CTracer - self._core_kwargs = {} - self.file_disposition_class = CFileDisposition - self.supports_plugins = True - self.packed_arcs = True - self.systrace = True - elif core == "pytrace": - self._trace_class = PyTracer - self._core_kwargs = {} - self.file_disposition_class = FileDisposition - self.supports_plugins = False - self.packed_arcs = False - self.systrace = True - else: - raise ConfigError(f"Unknown core value: {core!r}") - # We can handle a few concurrency options here, but only one at a time. concurrencies = set(self.concurrency) unknown = concurrencies - CoverageConfig.CONCURRENCY_CHOICES @@ -222,7 +160,7 @@ def __init__( msg = f"Couldn't trace with concurrency={tried}, the module isn't installed." raise ConfigError(msg) from ex - if self.concur_id_func and not hasattr(self._trace_class, "concur_id_func"): + if self.concur_id_func and not hasattr(core.tracer_class, "concur_id_func"): raise ConfigError( "Can't support concurrency={} with {}, only threads are supported.".format( tried, self.tracer_name(), @@ -249,7 +187,7 @@ def use_data(self, covdata: CoverageData, context: str | None) -> None: def tracer_name(self) -> str: """Return the class name of the tracer we're using.""" - return self._trace_class.__name__ + return self.core.tracer_class.__name__ def _clear_data(self) -> None: """Clear out existing data, but stay ready for more collection.""" @@ -305,7 +243,7 @@ def reset(self) -> None: self.should_trace_cache = {} # Our active Tracers. - self.tracers: list[TracerCore] = [] + self.tracers: list[Tracer] = [] self._clear_data() @@ -321,7 +259,7 @@ def unlock_data(self) -> None: def _start_tracer(self) -> TTraceFn | None: """Start a new Tracer object, and store it in self.tracers.""" - tracer = self._trace_class(**self._core_kwargs) + tracer = self.core.tracer_class(**self.core.tracer_kwargs) tracer.data = self.data tracer.lock_data = self.lock_data tracer.unlock_data = self.unlock_data @@ -403,7 +341,7 @@ def start(self) -> None: # Install our installation tracer in threading, to jump-start other # threads. - if self.systrace and self.threading: + if self.core.systrace and self.threading: self.threading.settrace(self._installation_trace) def stop(self) -> None: @@ -440,7 +378,7 @@ def resume(self) -> None: """Resume tracing after a `pause`.""" for tracer in self.tracers: tracer.start() - if self.systrace: + if self.core.systrace: if self.threading: self.threading.settrace(self._installation_trace) else: @@ -523,7 +461,7 @@ def flush_data(self) -> bool: return False if self.branch: - if self.packed_arcs: + if self.core.packed_arcs: # Unpack the line number pairs packed into integers. See # tracer.c:CTracer_record_pair for the C code that creates # these packed ints. diff --git a/coverage/context.py b/coverage/context.py index c8ee71271..630d3d25a 100644 --- a/coverage/context.py +++ b/coverage/context.py @@ -6,12 +6,14 @@ from __future__ import annotations from types import FrameType -from typing import cast, Callable, Sequence +from typing import cast, Sequence + +from coverage.types import TShouldStartContextFn def combine_context_switchers( - context_switchers: Sequence[Callable[[FrameType], str | None]], -) -> Callable[[FrameType], str | None] | None: + context_switchers: Sequence[TShouldStartContextFn], +) -> TShouldStartContextFn | None: """Create a single context switcher from multiple switchers. `context_switchers` is a list of functions that take a frame as an diff --git a/coverage/control.py b/coverage/control.py index 583fed946..ca757e9e1 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -26,9 +26,10 @@ from coverage import env from coverage.annotate import AnnotateReporter -from coverage.collector import Collector, HAS_CTRACER +from coverage.collector import Collector from coverage.config import CoverageConfig, read_coverage_config from coverage.context import should_start_context_test_function, combine_context_switchers +from coverage.core import Core, HAS_CTRACER from coverage.data import CoverageData, combine_parallel_data from coverage.debug import ( DebugControl, NoDebugging, short_stack, write_formatted_info, relevant_environment_display, @@ -262,6 +263,7 @@ def __init__( # pylint: disable=too-many-arguments self._inorout: InOrOut | None = None self._plugins: Plugins = Plugins() self._data: CoverageData | None = None + self._core: Core | None = None self._collector: Collector | None = None self._metacov = False @@ -532,16 +534,20 @@ def _init_for_start(self) -> None: should_start_context = combine_context_switchers(context_switchers) + self._core = Core( + warn=self._warn, + timid=self.config.timid, + metacov=self._metacov, + ) self._collector = Collector( + core=self._core, should_trace=self._should_trace, check_include=self._check_include_omit_etc, should_start_context=should_start_context, file_mapper=self._file_mapper, - timid=self.config.timid, branch=self.config.branch, warn=self._warn, concurrency=concurrency, - metacov=self._metacov, ) suffix = self._data_suffix_specified @@ -563,7 +569,7 @@ def _init_for_start(self) -> None: self._collector.use_data(self._data, self.config.context) # Early warning if we aren't going to be able to support plugins. - if self._plugins.file_tracers and not self._collector.supports_plugins: + if self._plugins.file_tracers and not self._core.supports_plugins: self._warn( "Plugin file tracers ({}) aren't supported with {}".format( ", ".join( @@ -584,7 +590,7 @@ def _init_for_start(self) -> None: include_namespace_packages=self.config.include_namespace_packages, ) self._inorout.plugins = self._plugins - self._inorout.disp_class = self._collector.file_disposition_class + self._inorout.disp_class = self._core.file_disposition_class # It's useful to write debug info after initing for start. self._should_write_debug = True diff --git a/coverage/core.py b/coverage/core.py new file mode 100644 index 000000000..89e49c49b --- /dev/null +++ b/coverage/core.py @@ -0,0 +1,90 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Management of core choices.""" + +from __future__ import annotations + +import os +import sys +from typing import Any + +from coverage import env +from coverage.disposition import FileDisposition +from coverage.exceptions import ConfigError +from coverage.pytracer import PyTracer +from coverage.sysmon import SysMonitor +from coverage.types import TFileDisposition, Tracer, TWarnFn + + +try: + # Use the C extension code when we can, for speed. + from coverage.tracer import CTracer, CFileDisposition + HAS_CTRACER = True +except ImportError: + # Couldn't import the C extension, maybe it isn't built. + if os.getenv("COVERAGE_CORE") == "ctrace": # pragma: part covered + # During testing, we use the COVERAGE_CORE environment variable + # to indicate that we've fiddled with the environment to test this + # fallback code. If we thought we had a C tracer, but couldn't import + # it, then exit quickly and clearly instead of dribbling confusing + # errors. I'm using sys.exit here instead of an exception because an + # exception here causes all sorts of other noise in unittest. + sys.stderr.write("*** COVERAGE_CORE is 'ctrace' but can't import CTracer!\n") + sys.exit(1) + HAS_CTRACER = False + + +class Core: + """Information about the central technology enabling execution measurement.""" + + tracer_class: type[Tracer] + tracer_kwargs: dict[str, Any] + file_disposition_class: type[TFileDisposition] + supports_plugins: bool + packed_arcs: bool + systrace: bool + + def __init__(self, warn: TWarnFn, timid: bool, metacov: bool) -> None: + core_name: str | None + if timid: + core_name = "pytrace" + else: + core_name = os.getenv("COVERAGE_CORE") + + if core_name == "sysmon" and not env.PYBEHAVIOR.pep669: + warn("sys.monitoring isn't available, using default core", slug="no-sysmon") + core_name = None + + if not core_name: + # Once we're comfortable with sysmon as a default: + # if env.PYBEHAVIOR.pep669 and self.should_start_context is None: + # core_name = "sysmon" + if HAS_CTRACER: + core_name = "ctrace" + else: + core_name = "pytrace" + + if core_name == "sysmon": + self.tracer_class = SysMonitor + self.tracer_kwargs = {"tool_id": 3 if metacov else 1} + self.file_disposition_class = FileDisposition + self.supports_plugins = False + self.packed_arcs = False + self.systrace = False + elif core_name == "ctrace": + self.tracer_class = CTracer + self.tracer_kwargs = {} + self.file_disposition_class = CFileDisposition + self.supports_plugins = True + self.packed_arcs = True + self.systrace = True + elif core_name == "pytrace": + self.tracer_class = PyTracer + self.tracer_kwargs = {} + self.file_disposition_class = FileDisposition + self.supports_plugins = False + self.packed_arcs = False + self.systrace = True + else: + raise ConfigError(f"Unknown core value: {core_name!r}") diff --git a/coverage/inorout.py b/coverage/inorout.py index 5ea29edf1..4df54b2b4 100644 --- a/coverage/inorout.py +++ b/coverage/inorout.py @@ -321,7 +321,8 @@ def nope(disp: TFileDisposition, reason: str) -> TFileDisposition: # co_filename value. dunder_file = frame.f_globals and frame.f_globals.get("__file__") if dunder_file: - filename = source_for_file(dunder_file) + # Danger: __file__ can (rarely?) be of type Path. + filename = source_for_file(str(dunder_file)) if original_filename and not original_filename.startswith("<"): orig = os.path.basename(original_filename) if orig != os.path.basename(filename): diff --git a/coverage/parser.py b/coverage/parser.py index 19267a718..0f7e353a3 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -678,7 +678,7 @@ class NodeList(ast.AST): """ def __init__(self, body: Sequence[ast.AST]) -> None: self.body = body - self.lineno = body[0].lineno + self.lineno = body[0].lineno # type: ignore[attr-defined] # TODO: Shouldn't the cause messages join with "and" instead of "or"? @@ -824,7 +824,7 @@ def line_for_node(self, node: ast.AST) -> TLineNo: if handler is not None: return handler(node) else: - return node.lineno + return node.lineno # type: ignore[attr-defined, no-any-return] # First lines: _line__* # @@ -1020,10 +1020,10 @@ def _missing__While(self, node: ast.While) -> ast.AST | None: if not body_nodes: return None # Make a synthetic While-true node. - new_while = ast.While() - new_while.lineno = body_nodes.lineno - new_while.test = ast.Name() - new_while.test.lineno = body_nodes.lineno + new_while = ast.While() # type: ignore[call-arg] + new_while.lineno = body_nodes.lineno # type: ignore[attr-defined] + new_while.test = ast.Name() # type: ignore[call-arg] + new_while.test.lineno = body_nodes.lineno # type: ignore[attr-defined] new_while.test.id = "True" assert hasattr(body_nodes, "body") new_while.body = body_nodes.body diff --git a/coverage/phystokens.py b/coverage/phystokens.py index ec8959fb6..9fc36ecda 100644 --- a/coverage/phystokens.py +++ b/coverage/phystokens.py @@ -57,7 +57,15 @@ def _phys_tokens(toks: TokenInfos) -> TokenInfos: if last_ttext.endswith("\\"): inject_backslash = False elif ttype == token.STRING: - if "\n" in ttext and ttext.split("\n", 1)[0][-1] == "\\": + if last_line.endswith(last_ttext + "\\\n"): + # Deal with special cases like such code:: + # + # a = ["aaa",\ + # "bbb \ + # ccc"] + # + inject_backslash = True + elif "\n" in ttext and ttext.split("\n", 1)[0][-1] == "\\": # 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 diff --git a/coverage/pytracer.py b/coverage/pytracer.py index 00cacea15..8144e6566 100644 --- a/coverage/pytracer.py +++ b/coverage/pytracer.py @@ -16,8 +16,16 @@ from coverage import env from coverage.types import ( - TArc, TFileDisposition, TLineNo, TTraceData, TTraceFileData, TTraceFn, - TracerCore, TWarnFn, + TArc, + TFileDisposition, + TLineNo, + TShouldStartContextFn, + TShouldTraceFn, + TTraceData, + TTraceFileData, + TTraceFn, + TWarnFn, + Tracer, ) # We need the YIELD_VALUE opcode below, in a comparison-friendly form. @@ -36,7 +44,7 @@ THIS_FILE = __file__.rstrip("co") -class PyTracer(TracerCore): +class PyTracer(Tracer): """Python implementation of the raw data tracer.""" # Because of poor implementations of trace-function-manipulating tools, @@ -64,9 +72,9 @@ def __init__(self) -> None: # Attributes set from the collector: self.data: TTraceData self.trace_arcs = False - self.should_trace: Callable[[str, FrameType], TFileDisposition] + self.should_trace: TShouldTraceFn self.should_trace_cache: dict[str, TFileDisposition | None] - self.should_start_context: Callable[[FrameType], str | None] | None = None + self.should_start_context: TShouldStartContextFn | None = None self.switch_context: Callable[[str | None], None] | None = None self.lock_data: Callable[[], None] self.unlock_data: Callable[[], None] diff --git a/coverage/sysmon.py b/coverage/sysmon.py index 0c9e795c6..ef54292f3 100644 --- a/coverage/sysmon.py +++ b/coverage/sysmon.py @@ -29,9 +29,11 @@ TArc, TFileDisposition, TLineNo, + TShouldStartContextFn, + TShouldTraceFn, TTraceData, TTraceFileData, - TracerCore, + Tracer, TWarnFn, ) @@ -171,7 +173,7 @@ def bytes_to_lines(code: CodeType) -> dict[int, int]: return b2l -class SysMonitor(TracerCore): +class SysMonitor(Tracer): """Python implementation of the raw data tracer for PEP669 implementations.""" # One of these will be used across threads. Be careful. @@ -180,11 +182,11 @@ def __init__(self, tool_id: int) -> None: # Attributes set from the collector: self.data: TTraceData self.trace_arcs = False - self.should_trace: Callable[[str, FrameType], TFileDisposition] + self.should_trace: TShouldTraceFn self.should_trace_cache: dict[str, TFileDisposition | None] # TODO: should_start_context and switch_context are unused! # Change tests/testenv.py:DYN_CONTEXTS when this is updated. - self.should_start_context: Callable[[FrameType], str | None] | None = None + self.should_start_context: TShouldStartContextFn | None = None self.switch_context: Callable[[str | None], None] | None = None self.lock_data: Callable[[], None] self.unlock_data: Callable[[], None] diff --git a/coverage/tracer.pyi b/coverage/tracer.pyi index 58dd69f94..d850493ed 100644 --- a/coverage/tracer.pyi +++ b/coverage/tracer.pyi @@ -5,7 +5,7 @@ from typing import Any, Dict -from coverage.types import TFileDisposition, TTraceData, TTraceFn, TracerCore +from coverage.types import TFileDisposition, TTraceData, TTraceFn, Tracer class CFileDisposition(TFileDisposition): """CFileDisposition is in ctracer/filedisp.c""" @@ -18,7 +18,7 @@ class CFileDisposition(TFileDisposition): trace: Any def __init__(self) -> None: ... -class CTracer(TracerCore): +class CTracer(Tracer): """CTracer is in ctracer/tracer.c""" check_include: Any concur_id_func: Any diff --git a/coverage/types.py b/coverage/types.py index ff5c59cda..bfd2a4d15 100644 --- a/coverage/types.py +++ b/coverage/types.py @@ -78,14 +78,19 @@ class TFileDisposition(Protocol): TTraceData = Dict[str, TTraceFileData] -class TracerCore(Protocol): +# Functions passed into collectors. +TShouldTraceFn = Callable[[str, FrameType], TFileDisposition] +TCheckIncludeFn = Callable[[str, FrameType], bool] +TShouldStartContextFn = Callable[[FrameType], Union[str, None]] + +class Tracer(Protocol): """Anything that can report on Python execution.""" data: TTraceData trace_arcs: bool - should_trace: Callable[[str, FrameType], TFileDisposition] + should_trace: TShouldTraceFn should_trace_cache: Mapping[str, TFileDisposition | None] - should_start_context: Callable[[FrameType], str | None] | None + should_start_context: TShouldStartContextFn | None switch_context: Callable[[str | None], None] | None lock_data: Callable[[], None] unlock_data: Callable[[], None] diff --git a/coverage/version.py b/coverage/version.py index f1f2993ba..ffefd8e00 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -8,7 +8,7 @@ # version_info: same semantics as sys.version_info. # _dev: the .devN suffix if any. -version_info = (7, 6, 0, "final", 0) +version_info = (7, 6, 1, "final", 0) _dev = 0 diff --git a/doc/conf.py b/doc/conf.py index 3886abb7f..030a1a1c1 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -67,11 +67,11 @@ # @@@ editable copyright = "2009–2024, Ned Batchelder" # pylint: disable=redefined-builtin # The short X.Y.Z version. -version = "7.6.0" +version = "7.6.1" # The full version, including alpha/beta/rc tags. -release = "7.6.0" +release = "7.6.1" # The date of release, in "monthname day, year" format. -release_date = "July 11, 2024" +release_date = "August 4, 2024" # @@@ end rst_epilog = f""" diff --git a/doc/index.rst b/doc/index.rst index 748b963af..100823ada 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -18,7 +18,7 @@ supported on: .. PYVERSIONS -* Python 3.8 through 3.12, and 3.13.0b3. +* Python 3.8 through 3.12, and 3.13.0rc1, including free-threading. * PyPy3 versions 3.8 through 3.10. .. ifconfig:: prerelease diff --git a/doc/other.rst b/doc/other.rst index d0998b37f..8c8f71003 100644 --- a/doc/other.rst +++ b/doc/other.rst @@ -53,9 +53,9 @@ Language plugins Coverage.py plugins to enable coverage measurement of other languages. -* `django-coverage`__ measures the coverage of Django templates. +* `django-coverage-plugin`__ measures the coverage of Django templates. - __ https://pypi.org/project/django-coverage/ + __ https://pypi.org/project/django-coverage-plugin/ * `Cython`__ provides a plugin for measuring Cythonized code. diff --git a/doc/sample_html/class_index.html b/doc/sample_html/class_index.html index ddcea11fd..244ce8666 100644 --- a/doc/sample_html/class_index.html +++ b/doc/sample_html/class_index.html @@ -56,8 +56,8 @@

Classes

- coverage.py v7.6.0, - created at 2024-07-11 12:38 -0400 + coverage.py v7.6.1, + created at 2024-08-04 15:03 -0400

@@ -537,8 +537,8 @@

- coverage.py v7.6.0, - created at 2024-07-11 12:38 -0400 + coverage.py v7.6.1, + created at 2024-08-04 15:03 -0400

diff --git a/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html b/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html index 8d7d719c1..01df01b73 100644 --- a/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html +++ b/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html @@ -66,8 +66,8 @@

^ index     » next       - coverage.py v7.6.0, - created at 2024-07-11 12:38 -0400 + coverage.py v7.6.1, + created at 2024-08-04 15:03 -0400