From 7b8dec9a2ef9e0abe50607982b79e6454d0e4ec8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 1 Oct 2023 11:22:10 -0400 Subject: [PATCH] feat!: remove fullcoverage, it doesn't work in 3.13 CPython stopped using it in https://github.com/python/cpython/issues/88054 --- CHANGES.rst | 5 +++ MANIFEST.in | 1 - coverage/collector.py | 22 ++---------- coverage/ctracer/tracer.c | 23 ++---------- coverage/fullcoverage/encodings.py | 57 ------------------------------ pyproject.toml | 3 +- setup.py | 1 - tests/test_process.py | 24 ------------- 8 files changed, 11 insertions(+), 125 deletions(-) delete mode 100644 coverage/fullcoverage/encodings.py diff --git a/CHANGES.rst b/CHANGES.rst index 009723737..520a8863e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -34,9 +34,14 @@ Unreleased - The new 3.12 soft keyword ``type`` is properly bolded in HTML reports. +- Removed the "fullcoverage" feature used by CPython to measure the coverage of + early-imported standard library modules. CPython `stopped using it + <88054_>`_ in 2021, and it stopped working completely in Python 3.13. + .. _issue 1605: https://github.com/nedbat/coveragepy/pull/1605 .. _issue 1684: https://github.com/nedbat/coveragepy/issues/1684 .. _pull 1685: https://github.com/nedbat/coveragepy/pull/1685 +.. _88054: https://github.com/python/cpython/issues/88054 .. scriv-start-here diff --git a/MANIFEST.in b/MANIFEST.in index 1a781f37a..96b4d9ac5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -31,7 +31,6 @@ recursive-include lab * recursive-include .github * recursive-include coverage *.pyi -recursive-include coverage/fullcoverage *.py recursive-include coverage/ctracer *.c *.h recursive-include doc *.py *.in *.pip *.rst *.txt *.png diff --git a/coverage/collector.py b/coverage/collector.py index ca7f5d94b..4ee4c4652 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -11,7 +11,7 @@ from types import FrameType from typing import ( - cast, Any, Callable, Dict, List, Mapping, Optional, Set, Tuple, Type, TypeVar, + cast, Any, Callable, Dict, List, Mapping, Optional, Set, Type, TypeVar, ) from coverage import env @@ -24,7 +24,7 @@ from coverage.plugin import CoveragePlugin from coverage.pytracer import PyTracer from coverage.types import ( - TArc, TFileDisposition, TLineNo, TTraceData, TTraceFn, TTracer, TWarnFn, + TArc, TFileDisposition, TTraceData, TTraceFn, TTracer, TWarnFn, ) os = isolate_module(os) @@ -330,18 +330,9 @@ def start(self) -> None: self.tracers = [] - # Check to see whether we had a fullcoverage tracer installed. If so, - # get the stack frames it stashed away for us. - traces0: List[Tuple[Tuple[FrameType, str, Any], TLineNo]] = [] - fn0 = sys.gettrace() - if fn0: - tracer0 = getattr(fn0, '__self__', None) - if tracer0: - traces0 = getattr(tracer0, 'traces', []) - try: # Install the tracer on this thread. - fn = self._start_tracer() + self._start_tracer() except: if self._collectors: self._collectors[-1].resume() @@ -351,13 +342,6 @@ def start(self) -> None: # stack of collectors. self._collectors.append(self) - # Replay all the events from fullcoverage into the new trace function. - for (frame, event, arg), lineno in traces0: - try: - fn(frame, event, arg, lineno=lineno) - except TypeError as ex: - raise RuntimeError("fullcoverage must be run with the C trace function.") from ex - # Install our installation tracer in threading, to jump-start other # threads. if self.threading: diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c index 518171f8e..02394f319 100644 --- a/coverage/ctracer/tracer.c +++ b/coverage/ctracer/tracer.c @@ -857,14 +857,6 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse * means it must be callable to be used in sys.settrace(). * * So we make ourself callable, equivalent to invoking our trace function. - * - * To help with the process of replaying stored frames, this function has an - * optional keyword argument: - * - * def CTracer_call(frame, event, arg, lineno=0) - * - * If provided, the lineno argument is used as the line number, and the - * frame's f_lineno member is ignored. */ static PyObject * CTracer_call(CTracer *self, PyObject *args, PyObject *kwds) @@ -872,9 +864,7 @@ CTracer_call(CTracer *self, PyObject *args, PyObject *kwds) PyFrameObject *frame; PyObject *what_str; PyObject *arg; - int lineno = 0; int what; - int orig_lineno; PyObject *ret = NULL; PyObject * ascii = NULL; @@ -888,10 +878,10 @@ CTracer_call(CTracer *self, PyObject *args, PyObject *kwds) NULL }; - static char *kwlist[] = {"frame", "event", "arg", "lineno", NULL}; + static char *kwlist[] = {"frame", "event", "arg", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!O!O|i:Tracer_call", kwlist, - &PyFrame_Type, &frame, &PyUnicode_Type, &what_str, &arg, &lineno)) { + &PyFrame_Type, &frame, &PyUnicode_Type, &what_str, &arg)) { goto done; } @@ -913,21 +903,12 @@ CTracer_call(CTracer *self, PyObject *args, PyObject *kwds) Py_DECREF(ascii); #endif - /* Save off the frame's lineno, and use the forced one, if provided. */ - orig_lineno = frame->f_lineno; - if (lineno > 0) { - frame->f_lineno = lineno; - } - /* Invoke the C function, and return ourselves. */ if (CTracer_trace(self, frame, what, arg) == RET_OK) { Py_INCREF(self); ret = (PyObject *)self; } - /* Clean up. */ - frame->f_lineno = orig_lineno; - /* For better speed, install ourselves the C way so that future calls go directly to CTracer_trace, without this intermediate function. diff --git a/coverage/fullcoverage/encodings.py b/coverage/fullcoverage/encodings.py deleted file mode 100644 index 73bd5646e..000000000 --- a/coverage/fullcoverage/encodings.py +++ /dev/null @@ -1,57 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - -"""Imposter encodings module that installs a coverage-style tracer. - -This is NOT the encodings module; it is an imposter that sets up tracing -instrumentation and then replaces itself with the real encodings module. - -If the directory that holds this file is placed first in the PYTHONPATH when -using "coverage" to run Python's tests, then this file will become the very -first module imported by the internals of Python 3. It installs a -coverage.py-compatible trace function that can watch Standard Library modules -execute from the very earliest stages of Python's own boot process. This fixes -a problem with coverage.py - that it starts too late to trace the coverage of -many of the most fundamental modules in the Standard Library. - -DO NOT import other modules into here, it will interfere with the goal of this -code executing before all imports. This is why this file isn't type-checked. - -""" - -import sys - -class FullCoverageTracer: - def __init__(self): - # `traces` is a list of trace events. Frames are tricky: the same - # frame object is used for a whole scope, with new line numbers - # written into it. So in one scope, all the frame objects are the - # same object, and will eventually all will point to the last line - # executed. So we keep the line numbers alongside the frames. - # The list looks like: - # - # traces = [ - # ((frame, event, arg), lineno), ... - # ] - # - self.traces = [] - - def fullcoverage_trace(self, *args): - frame, event, arg = args - if frame.f_lineno is not None: - # https://bugs.python.org/issue46911 - self.traces.append((args, frame.f_lineno)) - return self.fullcoverage_trace - -sys.settrace(FullCoverageTracer().fullcoverage_trace) - -# Remove our own directory from sys.path; remove ourselves from -# sys.modules; and re-import "encodings", which will be the real package -# this time. Note that the delete from sys.modules dictionary has to -# happen last, since all of the symbols in this module will become None -# at that exact moment, including "sys". - -parentdir = max(filter(__file__.startswith, sys.path), key=len) -sys.path.remove(parentdir) -del sys.modules['encodings'] -import encodings diff --git a/pyproject.toml b/pyproject.toml index 7f093fb00..84fbac205 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,8 +26,7 @@ warn_unused_configs = true warn_unused_ignores = true exclude = """(?x)( - ^coverage/fullcoverage/encodings\\.py$ # can't import things into it. - | ^tests/balance_xdist_plugin\\.py$ # not part of our test suite. + ^tests/balance_xdist_plugin\\.py$ # not part of our test suite. )""" ## PYLINT diff --git a/setup.py b/setup.py index 5222adea0..536b64ac9 100644 --- a/setup.py +++ b/setup.py @@ -87,7 +87,6 @@ package_data={ 'coverage': [ 'htmlfiles/*.*', - 'fullcoverage/*.*', 'py.typed', ] }, diff --git a/tests/test_process.py b/tests/test_process.py index 73adfe07e..0e33c6799 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -556,30 +556,6 @@ def f(): ) assert msg in out - @pytest.mark.expensive - @pytest.mark.skipif(not env.C_TRACER, reason="fullcoverage only works with the C tracer.") - @pytest.mark.skipif(env.METACOV, reason="Can't test fullcoverage when measuring ourselves") - def test_fullcoverage(self) -> None: - # fullcoverage is a trick to get stdlib modules measured from - # the very beginning of the process. Here we import os and - # then check how many lines are measured. - self.make_file("getenv.py", """\ - import os - print("FOOEY == %s" % os.getenv("FOOEY")) - """) - - fullcov = os.path.join(os.path.dirname(coverage.__file__), "fullcoverage") - self.set_environ("FOOEY", "BOO") - self.set_environ("PYTHONPATH", fullcov) - out = self.run_command("python -X frozen_modules=off -m coverage run -L getenv.py") - assert out == "FOOEY == BOO\n" - data = coverage.CoverageData() - data.read() - # The actual number of executed lines in os.py when it's - # imported is 120 or so. Just running os.getenv executes - # about 5. - assert line_counts(data)['os.py'] > 50 - # Pypy passes locally, but fails in CI? Perhaps the version of macOS is # significant? https://foss.heptapod.net/pypy/pypy/-/issues/3074 @pytest.mark.skipif(env.PYPY, reason="PyPy is unreliable with this test")