mirror of
https://github.com/pre-commit/pre-commit-hooks.git
synced 2026-04-06 12:06:53 +00:00
[pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
This commit is contained in:
parent
72ad6dc953
commit
f4cd1ba0d6
813 changed files with 66015 additions and 58839 deletions
|
|
@ -1,6 +1,5 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""
|
||||
Code coverage measurement for Python.
|
||||
|
||||
|
|
@ -8,31 +7,22 @@ Ned Batchelder
|
|||
https://coverage.readthedocs.io
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from coverage.control import Coverage as Coverage
|
||||
from coverage.control import process_startup as process_startup
|
||||
from coverage.data import CoverageData as CoverageData
|
||||
from coverage.exceptions import CoverageException as CoverageException
|
||||
from coverage.plugin import CoveragePlugin as CoveragePlugin
|
||||
from coverage.plugin import FileReporter as FileReporter
|
||||
from coverage.plugin import FileTracer as FileTracer
|
||||
from coverage.version import __version__ as __version__
|
||||
from coverage.version import version_info as version_info
|
||||
# mypy's convention is that "import as" names are public from the module.
|
||||
# We import names as themselves to indicate that. Pylint sees it as pointless,
|
||||
# so disable its warning.
|
||||
# pylint: disable=useless-import-alias
|
||||
|
||||
from coverage.version import (
|
||||
__version__ as __version__,
|
||||
version_info as version_info,
|
||||
)
|
||||
|
||||
from coverage.control import (
|
||||
Coverage as Coverage,
|
||||
process_startup as process_startup,
|
||||
)
|
||||
from coverage.data import CoverageData as CoverageData
|
||||
from coverage.exceptions import CoverageException as CoverageException
|
||||
from coverage.plugin import (
|
||||
CoveragePlugin as CoveragePlugin,
|
||||
FileReporter as FileReporter,
|
||||
FileTracer as FileTracer,
|
||||
)
|
||||
|
||||
# Backward compatibility.
|
||||
coverage = Coverage
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Coverage.py's main entry point."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from coverage.cmdline import main
|
||||
sys.exit(main())
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Source file annotation for coverage.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from typing import Iterable, TYPE_CHECKING
|
||||
from typing import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from coverage.files import flat_rootname
|
||||
from coverage.misc import ensure_dir, isolate_module
|
||||
from coverage.misc import ensure_dir
|
||||
from coverage.misc import isolate_module
|
||||
from coverage.plugin import FileReporter
|
||||
from coverage.report_core import get_analysis_to_report
|
||||
from coverage.results import Analysis
|
||||
|
|
@ -50,8 +49,8 @@ class AnnotateReporter:
|
|||
self.config = self.coverage.config
|
||||
self.directory: str | None = None
|
||||
|
||||
blank_re = re.compile(r"\s*(#|$)")
|
||||
else_re = re.compile(r"\s*else\s*:\s*(#|$)")
|
||||
blank_re = re.compile(r'\s*(#|$)')
|
||||
else_re = re.compile(r'\s*else\s*:\s*(#|$)')
|
||||
|
||||
def report(self, morfs: Iterable[TMorf] | None, directory: str | None = None) -> None:
|
||||
"""Run the report.
|
||||
|
|
@ -77,13 +76,13 @@ class AnnotateReporter:
|
|||
if self.directory:
|
||||
ensure_dir(self.directory)
|
||||
dest_file = os.path.join(self.directory, flat_rootname(fr.relative_filename()))
|
||||
if dest_file.endswith("_py"):
|
||||
dest_file = dest_file[:-3] + ".py"
|
||||
dest_file += ",cover"
|
||||
if dest_file.endswith('_py'):
|
||||
dest_file = dest_file[:-3] + '.py'
|
||||
dest_file += ',cover'
|
||||
else:
|
||||
dest_file = fr.filename + ",cover"
|
||||
dest_file = fr.filename + ',cover'
|
||||
|
||||
with open(dest_file, "w", encoding="utf-8") as dest:
|
||||
with open(dest_file, 'w', encoding='utf-8') as dest:
|
||||
i = j = 0
|
||||
covered = True
|
||||
source = fr.source()
|
||||
|
|
@ -95,20 +94,20 @@ class AnnotateReporter:
|
|||
if i < len(statements) and statements[i] == lineno:
|
||||
covered = j >= len(missing) or missing[j] > lineno
|
||||
if self.blank_re.match(line):
|
||||
dest.write(" ")
|
||||
dest.write(' ')
|
||||
elif self.else_re.match(line):
|
||||
# Special logic for lines containing only "else:".
|
||||
if j >= len(missing):
|
||||
dest.write("> ")
|
||||
dest.write('> ')
|
||||
elif statements[i] == missing[j]:
|
||||
dest.write("! ")
|
||||
dest.write('! ')
|
||||
else:
|
||||
dest.write("> ")
|
||||
dest.write('> ')
|
||||
elif lineno in excluded:
|
||||
dest.write("- ")
|
||||
dest.write('- ')
|
||||
elif covered:
|
||||
dest.write("> ")
|
||||
dest.write('> ')
|
||||
else:
|
||||
dest.write("! ")
|
||||
dest.write('! ')
|
||||
|
||||
dest.write(line)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Bytecode manipulation for coverage.py"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import CodeType
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Command-line support for coverage.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import optparse # pylint: disable=deprecated-module
|
||||
import os
|
||||
import os.path
|
||||
import shlex
|
||||
import sys
|
||||
import textwrap
|
||||
import traceback
|
||||
|
||||
from typing import cast, Any, NoReturn
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import NoReturn
|
||||
|
||||
import coverage
|
||||
from coverage import Coverage
|
||||
|
|
@ -22,16 +20,23 @@ from coverage import env
|
|||
from coverage.collector import HAS_CTRACER
|
||||
from coverage.config import CoverageConfig
|
||||
from coverage.control import DEFAULT_DATAFILE
|
||||
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
|
||||
from coverage.data import combinable_files
|
||||
from coverage.data import debug_data_file
|
||||
from coverage.debug import info_header
|
||||
from coverage.debug import short_stack
|
||||
from coverage.debug import write_formatted_info
|
||||
from coverage.exceptions import _BaseCoverageException
|
||||
from coverage.exceptions import _ExceptionDuringRun
|
||||
from coverage.exceptions import NoSource
|
||||
from coverage.execfile import PyRunner
|
||||
from coverage.results import Numbers, should_fail_under
|
||||
from coverage.results import Numbers
|
||||
from coverage.results import should_fail_under
|
||||
from coverage.version import __url__
|
||||
|
||||
# When adding to this file, alphabetization is important. Look for
|
||||
# "alphabetize" comments throughout.
|
||||
|
||||
|
||||
class Opts:
|
||||
"""A namespace class for individual options we'll build parsers from."""
|
||||
|
||||
|
|
@ -39,193 +44,193 @@ class Opts:
|
|||
# appears on the command line.
|
||||
|
||||
append = optparse.make_option(
|
||||
"-a", "--append", action="store_true",
|
||||
help="Append coverage data to .coverage, otherwise it starts clean each time.",
|
||||
'-a', '--append', action='store_true',
|
||||
help='Append coverage data to .coverage, otherwise it starts clean each time.',
|
||||
)
|
||||
branch = optparse.make_option(
|
||||
"", "--branch", action="store_true",
|
||||
help="Measure branch coverage in addition to statement coverage.",
|
||||
'', '--branch', action='store_true',
|
||||
help='Measure branch coverage in addition to statement coverage.',
|
||||
)
|
||||
concurrency = optparse.make_option(
|
||||
"", "--concurrency", action="store", metavar="LIBS",
|
||||
'', '--concurrency', action='store', metavar='LIBS',
|
||||
help=(
|
||||
"Properly measure code using a concurrency library. " +
|
||||
"Valid values are: {}, or a comma-list of them."
|
||||
).format(", ".join(sorted(CoverageConfig.CONCURRENCY_CHOICES))),
|
||||
'Properly measure code using a concurrency library. ' +
|
||||
'Valid values are: {}, or a comma-list of them.'
|
||||
).format(', '.join(sorted(CoverageConfig.CONCURRENCY_CHOICES))),
|
||||
)
|
||||
context = optparse.make_option(
|
||||
"", "--context", action="store", metavar="LABEL",
|
||||
help="The context label to record for this coverage run.",
|
||||
'', '--context', action='store', metavar='LABEL',
|
||||
help='The context label to record for this coverage run.',
|
||||
)
|
||||
contexts = optparse.make_option(
|
||||
"", "--contexts", action="store", metavar="REGEX1,REGEX2,...",
|
||||
'', '--contexts', action='store', metavar='REGEX1,REGEX2,...',
|
||||
help=(
|
||||
"Only display data from lines covered in the given contexts. " +
|
||||
"Accepts Python regexes, which must be quoted."
|
||||
'Only display data from lines covered in the given contexts. ' +
|
||||
'Accepts Python regexes, which must be quoted.'
|
||||
),
|
||||
)
|
||||
datafile = optparse.make_option(
|
||||
"", "--data-file", action="store", metavar="DATAFILE",
|
||||
'', '--data-file', action='store', metavar='DATAFILE',
|
||||
help=(
|
||||
"Base name of the data files to operate on. " +
|
||||
'Base name of the data files to operate on. ' +
|
||||
"Defaults to '.coverage'. [env: COVERAGE_FILE]"
|
||||
),
|
||||
)
|
||||
datafle_input = optparse.make_option(
|
||||
"", "--data-file", action="store", metavar="INFILE",
|
||||
'', '--data-file', action='store', metavar='INFILE',
|
||||
help=(
|
||||
"Read coverage data for report generation from this file. " +
|
||||
'Read coverage data for report generation from this file. ' +
|
||||
"Defaults to '.coverage'. [env: COVERAGE_FILE]"
|
||||
),
|
||||
)
|
||||
datafile_output = optparse.make_option(
|
||||
"", "--data-file", action="store", metavar="OUTFILE",
|
||||
'', '--data-file', action='store', metavar='OUTFILE',
|
||||
help=(
|
||||
"Write the recorded coverage data to this file. " +
|
||||
'Write the recorded coverage data to this file. ' +
|
||||
"Defaults to '.coverage'. [env: COVERAGE_FILE]"
|
||||
),
|
||||
)
|
||||
debug = optparse.make_option(
|
||||
"", "--debug", action="store", metavar="OPTS",
|
||||
help="Debug options, separated by commas. [env: COVERAGE_DEBUG]",
|
||||
'', '--debug', action='store', metavar='OPTS',
|
||||
help='Debug options, separated by commas. [env: COVERAGE_DEBUG]',
|
||||
)
|
||||
directory = optparse.make_option(
|
||||
"-d", "--directory", action="store", metavar="DIR",
|
||||
help="Write the output files to DIR.",
|
||||
'-d', '--directory', action='store', metavar='DIR',
|
||||
help='Write the output files to DIR.',
|
||||
)
|
||||
fail_under = optparse.make_option(
|
||||
"", "--fail-under", action="store", metavar="MIN", type="float",
|
||||
help="Exit with a status of 2 if the total coverage is less than MIN.",
|
||||
'', '--fail-under', action='store', metavar='MIN', type='float',
|
||||
help='Exit with a status of 2 if the total coverage is less than MIN.',
|
||||
)
|
||||
format = optparse.make_option(
|
||||
"", "--format", action="store", metavar="FORMAT",
|
||||
help="Output format, either text (default), markdown, or total.",
|
||||
'', '--format', action='store', metavar='FORMAT',
|
||||
help='Output format, either text (default), markdown, or total.',
|
||||
)
|
||||
help = optparse.make_option(
|
||||
"-h", "--help", action="store_true",
|
||||
help="Get help on this command.",
|
||||
'-h', '--help', action='store_true',
|
||||
help='Get help on this command.',
|
||||
)
|
||||
ignore_errors = optparse.make_option(
|
||||
"-i", "--ignore-errors", action="store_true",
|
||||
help="Ignore errors while reading source files.",
|
||||
'-i', '--ignore-errors', action='store_true',
|
||||
help='Ignore errors while reading source files.',
|
||||
)
|
||||
include = optparse.make_option(
|
||||
"", "--include", action="store", metavar="PAT1,PAT2,...",
|
||||
'', '--include', action='store', metavar='PAT1,PAT2,...',
|
||||
help=(
|
||||
"Include only files whose paths match one of these patterns. " +
|
||||
"Accepts shell-style wildcards, which must be quoted."
|
||||
'Include only files whose paths match one of these patterns. ' +
|
||||
'Accepts shell-style wildcards, which must be quoted.'
|
||||
),
|
||||
)
|
||||
keep = optparse.make_option(
|
||||
"", "--keep", action="store_true",
|
||||
help="Keep original coverage files, otherwise they are deleted.",
|
||||
'', '--keep', action='store_true',
|
||||
help='Keep original coverage files, otherwise they are deleted.',
|
||||
)
|
||||
pylib = optparse.make_option(
|
||||
"-L", "--pylib", action="store_true",
|
||||
'-L', '--pylib', action='store_true',
|
||||
help=(
|
||||
"Measure coverage even inside the Python installed library, " +
|
||||
'Measure coverage even inside the Python installed library, ' +
|
||||
"which isn't done by default."
|
||||
),
|
||||
)
|
||||
show_missing = optparse.make_option(
|
||||
"-m", "--show-missing", action="store_true",
|
||||
'-m', '--show-missing', action='store_true',
|
||||
help="Show line numbers of statements in each module that weren't executed.",
|
||||
)
|
||||
module = optparse.make_option(
|
||||
"-m", "--module", action="store_true",
|
||||
'-m', '--module', action='store_true',
|
||||
help=(
|
||||
"<pyfile> is an importable Python module, not a script path, " +
|
||||
'<pyfile> is an importable Python module, not a script path, ' +
|
||||
"to be run as 'python -m' would run it."
|
||||
),
|
||||
)
|
||||
omit = optparse.make_option(
|
||||
"", "--omit", action="store", metavar="PAT1,PAT2,...",
|
||||
'', '--omit', action='store', metavar='PAT1,PAT2,...',
|
||||
help=(
|
||||
"Omit files whose paths match one of these patterns. " +
|
||||
"Accepts shell-style wildcards, which must be quoted."
|
||||
'Omit files whose paths match one of these patterns. ' +
|
||||
'Accepts shell-style wildcards, which must be quoted.'
|
||||
),
|
||||
)
|
||||
output_xml = optparse.make_option(
|
||||
"-o", "", action="store", dest="outfile", metavar="OUTFILE",
|
||||
'-o', '', action='store', dest='outfile', metavar='OUTFILE',
|
||||
help="Write the XML report to this file. Defaults to 'coverage.xml'",
|
||||
)
|
||||
output_json = optparse.make_option(
|
||||
"-o", "", action="store", dest="outfile", metavar="OUTFILE",
|
||||
'-o', '', action='store', dest='outfile', metavar='OUTFILE',
|
||||
help="Write the JSON report to this file. Defaults to 'coverage.json'",
|
||||
)
|
||||
output_lcov = optparse.make_option(
|
||||
"-o", "", action="store", dest="outfile", metavar="OUTFILE",
|
||||
'-o', '', action='store', dest='outfile', metavar='OUTFILE',
|
||||
help="Write the LCOV report to this file. Defaults to 'coverage.lcov'",
|
||||
)
|
||||
json_pretty_print = optparse.make_option(
|
||||
"", "--pretty-print", action="store_true",
|
||||
help="Format the JSON for human readers.",
|
||||
'', '--pretty-print', action='store_true',
|
||||
help='Format the JSON for human readers.',
|
||||
)
|
||||
parallel_mode = optparse.make_option(
|
||||
"-p", "--parallel-mode", action="store_true",
|
||||
'-p', '--parallel-mode', action='store_true',
|
||||
help=(
|
||||
"Append the machine name, process id and random number to the " +
|
||||
"data file name to simplify collecting data from " +
|
||||
"many processes."
|
||||
'Append the machine name, process id and random number to the ' +
|
||||
'data file name to simplify collecting data from ' +
|
||||
'many processes.'
|
||||
),
|
||||
)
|
||||
precision = optparse.make_option(
|
||||
"", "--precision", action="store", metavar="N", type=int,
|
||||
'', '--precision', action='store', metavar='N', type=int,
|
||||
help=(
|
||||
"Number of digits after the decimal point to display for " +
|
||||
"reported coverage percentages."
|
||||
'Number of digits after the decimal point to display for ' +
|
||||
'reported coverage percentages.'
|
||||
),
|
||||
)
|
||||
quiet = optparse.make_option(
|
||||
"-q", "--quiet", action="store_true",
|
||||
'-q', '--quiet', action='store_true',
|
||||
help="Don't print messages about what is happening.",
|
||||
)
|
||||
rcfile = optparse.make_option(
|
||||
"", "--rcfile", action="store",
|
||||
'', '--rcfile', action='store',
|
||||
help=(
|
||||
"Specify configuration file. " +
|
||||
'Specify configuration file. ' +
|
||||
"By default '.coveragerc', 'setup.cfg', 'tox.ini', and " +
|
||||
"'pyproject.toml' are tried. [env: COVERAGE_RCFILE]"
|
||||
),
|
||||
)
|
||||
show_contexts = optparse.make_option(
|
||||
"--show-contexts", action="store_true",
|
||||
help="Show contexts for covered lines.",
|
||||
'--show-contexts', action='store_true',
|
||||
help='Show contexts for covered lines.',
|
||||
)
|
||||
skip_covered = optparse.make_option(
|
||||
"--skip-covered", action="store_true",
|
||||
help="Skip files with 100% coverage.",
|
||||
'--skip-covered', action='store_true',
|
||||
help='Skip files with 100% coverage.',
|
||||
)
|
||||
no_skip_covered = optparse.make_option(
|
||||
"--no-skip-covered", action="store_false", dest="skip_covered",
|
||||
help="Disable --skip-covered.",
|
||||
'--no-skip-covered', action='store_false', dest='skip_covered',
|
||||
help='Disable --skip-covered.',
|
||||
)
|
||||
skip_empty = optparse.make_option(
|
||||
"--skip-empty", action="store_true",
|
||||
help="Skip files with no code.",
|
||||
'--skip-empty', action='store_true',
|
||||
help='Skip files with no code.',
|
||||
)
|
||||
sort = optparse.make_option(
|
||||
"--sort", action="store", metavar="COLUMN",
|
||||
'--sort', action='store', metavar='COLUMN',
|
||||
help=(
|
||||
"Sort the report by the named column: name, stmts, miss, branch, brpart, or cover. " +
|
||||
"Default is name."
|
||||
'Sort the report by the named column: name, stmts, miss, branch, brpart, or cover. ' +
|
||||
'Default is name.'
|
||||
),
|
||||
)
|
||||
source = optparse.make_option(
|
||||
"", "--source", action="store", metavar="SRC1,SRC2,...",
|
||||
help="A list of directories or importable names of code to measure.",
|
||||
'', '--source', action='store', metavar='SRC1,SRC2,...',
|
||||
help='A list of directories or importable names of code to measure.',
|
||||
)
|
||||
timid = optparse.make_option(
|
||||
"", "--timid", action="store_true",
|
||||
help="Use the slower Python trace function core.",
|
||||
'', '--timid', action='store_true',
|
||||
help='Use the slower Python trace function core.',
|
||||
)
|
||||
title = optparse.make_option(
|
||||
"", "--title", action="store", metavar="TITLE",
|
||||
help="A text string to use as the title on the HTML.",
|
||||
'', '--title', action='store', metavar='TITLE',
|
||||
help='A text string to use as the title on the HTML.',
|
||||
)
|
||||
version = optparse.make_option(
|
||||
"", "--version", action="store_true",
|
||||
help="Display version information and exit.",
|
||||
'', '--version', action='store_true',
|
||||
help='Display version information and exit.',
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -238,7 +243,7 @@ class CoverageOptionParser(optparse.OptionParser):
|
|||
"""
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
kwargs["add_help_option"] = False
|
||||
kwargs['add_help_option'] = False
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_defaults(
|
||||
# Keep these arguments alphabetized by their names.
|
||||
|
|
@ -330,7 +335,7 @@ class CmdOptionParser(CoverageOptionParser):
|
|||
|
||||
"""
|
||||
if usage:
|
||||
usage = "%prog " + usage
|
||||
usage = '%prog ' + usage
|
||||
super().__init__(
|
||||
usage=usage,
|
||||
description=description,
|
||||
|
|
@ -342,7 +347,7 @@ class CmdOptionParser(CoverageOptionParser):
|
|||
def __eq__(self, other: str) -> bool: # type: ignore[override]
|
||||
# A convenience equality, so that I can put strings in unit test
|
||||
# results, and they will compare equal to objects.
|
||||
return (other == f"<CmdOptionParser:{self.cmd}>")
|
||||
return (other == f'<CmdOptionParser:{self.cmd}>')
|
||||
|
||||
__hash__ = None # type: ignore[assignment]
|
||||
|
||||
|
|
@ -351,7 +356,7 @@ class CmdOptionParser(CoverageOptionParser):
|
|||
program_name = super().get_prog_name()
|
||||
|
||||
# Include the sub-command for this parser as part of the command.
|
||||
return f"{program_name} {self.cmd}"
|
||||
return f'{program_name} {self.cmd}'
|
||||
|
||||
# In lists of Opts, keep them alphabetized by the option names as they appear
|
||||
# on the command line, since these lists determine the order of the options in
|
||||
|
|
@ -359,6 +364,7 @@ class CmdOptionParser(CoverageOptionParser):
|
|||
#
|
||||
# In COMMANDS, keep the keys (command names) alphabetized.
|
||||
|
||||
|
||||
GLOBAL_ARGS = [
|
||||
Opts.debug,
|
||||
Opts.help,
|
||||
|
|
@ -366,72 +372,72 @@ GLOBAL_ARGS = [
|
|||
]
|
||||
|
||||
COMMANDS = {
|
||||
"annotate": CmdOptionParser(
|
||||
"annotate",
|
||||
'annotate': CmdOptionParser(
|
||||
'annotate',
|
||||
[
|
||||
Opts.directory,
|
||||
Opts.datafle_input,
|
||||
Opts.ignore_errors,
|
||||
Opts.include,
|
||||
Opts.omit,
|
||||
] + GLOBAL_ARGS,
|
||||
usage="[options] [modules]",
|
||||
] + GLOBAL_ARGS,
|
||||
usage='[options] [modules]',
|
||||
description=(
|
||||
"Make annotated copies of the given files, marking statements that are executed " +
|
||||
"with > and statements that are missed with !."
|
||||
'Make annotated copies of the given files, marking statements that are executed ' +
|
||||
'with > and statements that are missed with !.'
|
||||
),
|
||||
),
|
||||
|
||||
"combine": CmdOptionParser(
|
||||
"combine",
|
||||
'combine': CmdOptionParser(
|
||||
'combine',
|
||||
[
|
||||
Opts.append,
|
||||
Opts.datafile,
|
||||
Opts.keep,
|
||||
Opts.quiet,
|
||||
] + GLOBAL_ARGS,
|
||||
usage="[options] <path1> <path2> ... <pathN>",
|
||||
] + GLOBAL_ARGS,
|
||||
usage='[options] <path1> <path2> ... <pathN>',
|
||||
description=(
|
||||
"Combine data from multiple coverage files. " +
|
||||
"The combined results are written to a single " +
|
||||
"file representing the union of the data. The positional " +
|
||||
"arguments are data files or directories containing data files. " +
|
||||
'Combine data from multiple coverage files. ' +
|
||||
'The combined results are written to a single ' +
|
||||
'file representing the union of the data. The positional ' +
|
||||
'arguments are data files or directories containing data files. ' +
|
||||
"If no paths are provided, data files in the default data file's " +
|
||||
"directory are combined."
|
||||
'directory are combined.'
|
||||
),
|
||||
),
|
||||
|
||||
"debug": CmdOptionParser(
|
||||
"debug", GLOBAL_ARGS,
|
||||
usage="<topic>",
|
||||
'debug': CmdOptionParser(
|
||||
'debug', GLOBAL_ARGS,
|
||||
usage='<topic>',
|
||||
description=(
|
||||
"Display information about the internals of coverage.py, " +
|
||||
"for diagnosing problems. " +
|
||||
"Topics are: " +
|
||||
"'data' to show a summary of the collected data; " +
|
||||
"'sys' to show installation information; " +
|
||||
"'config' to show the configuration; " +
|
||||
"'premain' to show what is calling coverage; " +
|
||||
"'pybehave' to show internal flags describing Python behavior."
|
||||
'Display information about the internals of coverage.py, ' +
|
||||
'for diagnosing problems. ' +
|
||||
'Topics are: ' +
|
||||
"'data' to show a summary of the collected data; " +
|
||||
"'sys' to show installation information; " +
|
||||
"'config' to show the configuration; " +
|
||||
"'premain' to show what is calling coverage; " +
|
||||
"'pybehave' to show internal flags describing Python behavior."
|
||||
),
|
||||
),
|
||||
|
||||
"erase": CmdOptionParser(
|
||||
"erase",
|
||||
'erase': CmdOptionParser(
|
||||
'erase',
|
||||
[
|
||||
Opts.datafile,
|
||||
] + GLOBAL_ARGS,
|
||||
description="Erase previously collected coverage data.",
|
||||
] + GLOBAL_ARGS,
|
||||
description='Erase previously collected coverage data.',
|
||||
),
|
||||
|
||||
"help": CmdOptionParser(
|
||||
"help", GLOBAL_ARGS,
|
||||
usage="[command]",
|
||||
description="Describe how to use coverage.py",
|
||||
'help': CmdOptionParser(
|
||||
'help', GLOBAL_ARGS,
|
||||
usage='[command]',
|
||||
description='Describe how to use coverage.py',
|
||||
),
|
||||
|
||||
"html": CmdOptionParser(
|
||||
"html",
|
||||
'html': CmdOptionParser(
|
||||
'html',
|
||||
[
|
||||
Opts.contexts,
|
||||
Opts.directory,
|
||||
|
|
@ -447,17 +453,17 @@ COMMANDS = {
|
|||
Opts.no_skip_covered,
|
||||
Opts.skip_empty,
|
||||
Opts.title,
|
||||
] + GLOBAL_ARGS,
|
||||
usage="[options] [modules]",
|
||||
] + GLOBAL_ARGS,
|
||||
usage='[options] [modules]',
|
||||
description=(
|
||||
"Create an HTML report of the coverage of the files. " +
|
||||
"Each file gets its own page, with the source decorated to show " +
|
||||
"executed, excluded, and missed lines."
|
||||
'Create an HTML report of the coverage of the files. ' +
|
||||
'Each file gets its own page, with the source decorated to show ' +
|
||||
'executed, excluded, and missed lines.'
|
||||
),
|
||||
),
|
||||
|
||||
"json": CmdOptionParser(
|
||||
"json",
|
||||
'json': CmdOptionParser(
|
||||
'json',
|
||||
[
|
||||
Opts.contexts,
|
||||
Opts.datafle_input,
|
||||
|
|
@ -469,13 +475,13 @@ COMMANDS = {
|
|||
Opts.json_pretty_print,
|
||||
Opts.quiet,
|
||||
Opts.show_contexts,
|
||||
] + GLOBAL_ARGS,
|
||||
usage="[options] [modules]",
|
||||
description="Generate a JSON report of coverage results.",
|
||||
] + GLOBAL_ARGS,
|
||||
usage='[options] [modules]',
|
||||
description='Generate a JSON report of coverage results.',
|
||||
),
|
||||
|
||||
"lcov": CmdOptionParser(
|
||||
"lcov",
|
||||
'lcov': CmdOptionParser(
|
||||
'lcov',
|
||||
[
|
||||
Opts.datafle_input,
|
||||
Opts.fail_under,
|
||||
|
|
@ -484,13 +490,13 @@ COMMANDS = {
|
|||
Opts.output_lcov,
|
||||
Opts.omit,
|
||||
Opts.quiet,
|
||||
] + GLOBAL_ARGS,
|
||||
usage="[options] [modules]",
|
||||
description="Generate an LCOV report of coverage results.",
|
||||
] + GLOBAL_ARGS,
|
||||
usage='[options] [modules]',
|
||||
description='Generate an LCOV report of coverage results.',
|
||||
),
|
||||
|
||||
"report": CmdOptionParser(
|
||||
"report",
|
||||
'report': CmdOptionParser(
|
||||
'report',
|
||||
[
|
||||
Opts.contexts,
|
||||
Opts.datafle_input,
|
||||
|
|
@ -505,13 +511,13 @@ COMMANDS = {
|
|||
Opts.skip_covered,
|
||||
Opts.no_skip_covered,
|
||||
Opts.skip_empty,
|
||||
] + GLOBAL_ARGS,
|
||||
usage="[options] [modules]",
|
||||
description="Report coverage statistics on modules.",
|
||||
] + GLOBAL_ARGS,
|
||||
usage='[options] [modules]',
|
||||
description='Report coverage statistics on modules.',
|
||||
),
|
||||
|
||||
"run": CmdOptionParser(
|
||||
"run",
|
||||
'run': CmdOptionParser(
|
||||
'run',
|
||||
[
|
||||
Opts.append,
|
||||
Opts.branch,
|
||||
|
|
@ -525,13 +531,13 @@ COMMANDS = {
|
|||
Opts.parallel_mode,
|
||||
Opts.source,
|
||||
Opts.timid,
|
||||
] + GLOBAL_ARGS,
|
||||
usage="[options] <pyfile> [program options]",
|
||||
description="Run a Python program, measuring code execution.",
|
||||
] + GLOBAL_ARGS,
|
||||
usage='[options] <pyfile> [program options]',
|
||||
description='Run a Python program, measuring code execution.',
|
||||
),
|
||||
|
||||
"xml": CmdOptionParser(
|
||||
"xml",
|
||||
'xml': CmdOptionParser(
|
||||
'xml',
|
||||
[
|
||||
Opts.datafle_input,
|
||||
Opts.fail_under,
|
||||
|
|
@ -541,9 +547,9 @@ COMMANDS = {
|
|||
Opts.output_xml,
|
||||
Opts.quiet,
|
||||
Opts.skip_empty,
|
||||
] + GLOBAL_ARGS,
|
||||
usage="[options] [modules]",
|
||||
description="Generate an XML report of coverage results.",
|
||||
] + GLOBAL_ARGS,
|
||||
usage='[options] [modules]',
|
||||
description='Generate an XML report of coverage results.',
|
||||
),
|
||||
}
|
||||
|
||||
|
|
@ -557,7 +563,7 @@ def show_help(
|
|||
assert error or topic or parser
|
||||
|
||||
program_path = sys.argv[0]
|
||||
if program_path.endswith(os.path.sep + "__main__.py"):
|
||||
if program_path.endswith(os.path.sep + '__main__.py'):
|
||||
# The path is the main module of a package; get that path instead.
|
||||
program_path = os.path.dirname(program_path)
|
||||
program_name = os.path.basename(program_path)
|
||||
|
|
@ -567,17 +573,17 @@ def show_help(
|
|||
# invoke coverage-script.py, coverage3-script.py, and
|
||||
# coverage-3.5-script.py. argv[0] is the .py file, but we want to
|
||||
# get back to the original form.
|
||||
auto_suffix = "-script.py"
|
||||
auto_suffix = '-script.py'
|
||||
if program_name.endswith(auto_suffix):
|
||||
program_name = program_name[:-len(auto_suffix)]
|
||||
|
||||
help_params = dict(coverage.__dict__)
|
||||
help_params["__url__"] = __url__
|
||||
help_params["program_name"] = program_name
|
||||
help_params['__url__'] = __url__
|
||||
help_params['program_name'] = program_name
|
||||
if HAS_CTRACER:
|
||||
help_params["extension_modifier"] = "with C extension"
|
||||
help_params['extension_modifier'] = 'with C extension'
|
||||
else:
|
||||
help_params["extension_modifier"] = "without C extension"
|
||||
help_params['extension_modifier'] = 'without C extension'
|
||||
|
||||
if error:
|
||||
print(error, file=sys.stderr)
|
||||
|
|
@ -587,12 +593,12 @@ def show_help(
|
|||
print()
|
||||
else:
|
||||
assert topic is not None
|
||||
help_msg = textwrap.dedent(HELP_TOPICS.get(topic, "")).strip()
|
||||
help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip()
|
||||
if help_msg:
|
||||
print(help_msg.format(**help_params))
|
||||
else:
|
||||
print(f"Don't know topic {topic!r}")
|
||||
print("Full documentation is at {__url__}".format(**help_params))
|
||||
print('Full documentation is at {__url__}'.format(**help_params))
|
||||
|
||||
|
||||
OK, ERR, FAIL_UNDER = 0, 1, 2
|
||||
|
|
@ -615,19 +621,19 @@ class CoverageScript:
|
|||
"""
|
||||
# Collect the command-line options.
|
||||
if not argv:
|
||||
show_help(topic="minimum_help")
|
||||
show_help(topic='minimum_help')
|
||||
return OK
|
||||
|
||||
# The command syntax we parse depends on the first argument. Global
|
||||
# switch syntax always starts with an option.
|
||||
parser: optparse.OptionParser | None
|
||||
self.global_option = argv[0].startswith("-")
|
||||
self.global_option = argv[0].startswith('-')
|
||||
if self.global_option:
|
||||
parser = GlobalOptionParser()
|
||||
else:
|
||||
parser = COMMANDS.get(argv[0])
|
||||
if not parser:
|
||||
show_help(f"Unknown command: {argv[0]!r}")
|
||||
show_help(f'Unknown command: {argv[0]!r}')
|
||||
return ERR
|
||||
argv = argv[1:]
|
||||
|
||||
|
|
@ -648,7 +654,7 @@ class CoverageScript:
|
|||
contexts = unshell_list(options.contexts)
|
||||
|
||||
if options.concurrency is not None:
|
||||
concurrency = options.concurrency.split(",")
|
||||
concurrency = options.concurrency.split(',')
|
||||
else:
|
||||
concurrency = None
|
||||
|
||||
|
|
@ -670,17 +676,17 @@ class CoverageScript:
|
|||
messages=not options.quiet,
|
||||
)
|
||||
|
||||
if options.action == "debug":
|
||||
if options.action == 'debug':
|
||||
return self.do_debug(args)
|
||||
|
||||
elif options.action == "erase":
|
||||
elif options.action == 'erase':
|
||||
self.coverage.erase()
|
||||
return OK
|
||||
|
||||
elif options.action == "run":
|
||||
elif options.action == 'run':
|
||||
return self.do_run(options, args)
|
||||
|
||||
elif options.action == "combine":
|
||||
elif options.action == 'combine':
|
||||
if options.append:
|
||||
self.coverage.load()
|
||||
data_paths = args or None
|
||||
|
|
@ -699,12 +705,12 @@ class CoverageScript:
|
|||
|
||||
# We need to be able to import from the current directory, because
|
||||
# plugins may try to, for example, to read Django settings.
|
||||
sys.path.insert(0, "")
|
||||
sys.path.insert(0, '')
|
||||
|
||||
self.coverage.load()
|
||||
|
||||
total = None
|
||||
if options.action == "report":
|
||||
if options.action == 'report':
|
||||
total = self.coverage.report(
|
||||
precision=options.precision,
|
||||
show_missing=options.show_missing,
|
||||
|
|
@ -714,9 +720,9 @@ class CoverageScript:
|
|||
output_format=options.format,
|
||||
**report_args,
|
||||
)
|
||||
elif options.action == "annotate":
|
||||
elif options.action == 'annotate':
|
||||
self.coverage.annotate(directory=options.directory, **report_args)
|
||||
elif options.action == "html":
|
||||
elif options.action == 'html':
|
||||
total = self.coverage.html_report(
|
||||
directory=options.directory,
|
||||
precision=options.precision,
|
||||
|
|
@ -726,20 +732,20 @@ class CoverageScript:
|
|||
title=options.title,
|
||||
**report_args,
|
||||
)
|
||||
elif options.action == "xml":
|
||||
elif options.action == 'xml':
|
||||
total = self.coverage.xml_report(
|
||||
outfile=options.outfile,
|
||||
skip_empty=options.skip_empty,
|
||||
**report_args,
|
||||
)
|
||||
elif options.action == "json":
|
||||
elif options.action == 'json':
|
||||
total = self.coverage.json_report(
|
||||
outfile=options.outfile,
|
||||
pretty_print=options.pretty_print,
|
||||
show_contexts=options.show_contexts,
|
||||
**report_args,
|
||||
)
|
||||
elif options.action == "lcov":
|
||||
elif options.action == 'lcov':
|
||||
total = self.coverage.lcov_report(
|
||||
outfile=options.outfile,
|
||||
**report_args,
|
||||
|
|
@ -752,19 +758,19 @@ class CoverageScript:
|
|||
# Apply the command line fail-under options, and then use the config
|
||||
# value, so we can get fail_under from the config file.
|
||||
if options.fail_under is not None:
|
||||
self.coverage.set_option("report:fail_under", options.fail_under)
|
||||
self.coverage.set_option('report:fail_under', options.fail_under)
|
||||
if options.precision is not None:
|
||||
self.coverage.set_option("report:precision", options.precision)
|
||||
self.coverage.set_option('report:precision', options.precision)
|
||||
|
||||
fail_under = cast(float, self.coverage.get_option("report:fail_under"))
|
||||
precision = cast(int, self.coverage.get_option("report:precision"))
|
||||
fail_under = cast(float, self.coverage.get_option('report:fail_under'))
|
||||
precision = cast(int, self.coverage.get_option('report:precision'))
|
||||
if should_fail_under(total, fail_under, precision):
|
||||
msg = "total of {total} is less than fail-under={fail_under:.{p}f}".format(
|
||||
msg = 'total of {total} is less than fail-under={fail_under:.{p}f}'.format(
|
||||
total=Numbers(precision=precision).display_covered(total),
|
||||
fail_under=fail_under,
|
||||
p=precision,
|
||||
)
|
||||
print("Coverage failure:", msg)
|
||||
print('Coverage failure:', msg)
|
||||
return FAIL_UNDER
|
||||
|
||||
return OK
|
||||
|
|
@ -783,12 +789,12 @@ class CoverageScript:
|
|||
# Handle help.
|
||||
if options.help:
|
||||
if self.global_option:
|
||||
show_help(topic="help")
|
||||
show_help(topic='help')
|
||||
else:
|
||||
show_help(parser=parser)
|
||||
return True
|
||||
|
||||
if options.action == "help":
|
||||
if options.action == 'help':
|
||||
if args:
|
||||
for a in args:
|
||||
parser_maybe = COMMANDS.get(a)
|
||||
|
|
@ -797,12 +803,12 @@ class CoverageScript:
|
|||
else:
|
||||
show_help(topic=a)
|
||||
else:
|
||||
show_help(topic="help")
|
||||
show_help(topic='help')
|
||||
return True
|
||||
|
||||
# Handle version.
|
||||
if options.version:
|
||||
show_help(topic="version")
|
||||
show_help(topic='version')
|
||||
return True
|
||||
|
||||
return False
|
||||
|
|
@ -813,37 +819,37 @@ class CoverageScript:
|
|||
if not args:
|
||||
if options.module:
|
||||
# Specified -m with nothing else.
|
||||
show_help("No module specified for -m")
|
||||
show_help('No module specified for -m')
|
||||
return ERR
|
||||
command_line = cast(str, self.coverage.get_option("run:command_line"))
|
||||
command_line = cast(str, self.coverage.get_option('run:command_line'))
|
||||
if command_line is not None:
|
||||
args = shlex.split(command_line)
|
||||
if args and args[0] in {"-m", "--module"}:
|
||||
if args and args[0] in {'-m', '--module'}:
|
||||
options.module = True
|
||||
args = args[1:]
|
||||
if not args:
|
||||
show_help("Nothing to do.")
|
||||
show_help('Nothing to do.')
|
||||
return ERR
|
||||
|
||||
if options.append and self.coverage.get_option("run:parallel"):
|
||||
if options.append and self.coverage.get_option('run:parallel'):
|
||||
show_help("Can't append to data files in parallel mode.")
|
||||
return ERR
|
||||
|
||||
if options.concurrency == "multiprocessing":
|
||||
if options.concurrency == 'multiprocessing':
|
||||
# Can't set other run-affecting command line options with
|
||||
# multiprocessing.
|
||||
for opt_name in ["branch", "include", "omit", "pylib", "source", "timid"]:
|
||||
for opt_name in ['branch', 'include', 'omit', 'pylib', 'source', 'timid']:
|
||||
# As it happens, all of these options have no default, meaning
|
||||
# they will be None if they have not been specified.
|
||||
if getattr(options, opt_name) is not None:
|
||||
show_help(
|
||||
"Options affecting multiprocessing must only be specified " +
|
||||
"in a configuration file.\n" +
|
||||
f"Remove --{opt_name} from the command line.",
|
||||
'Options affecting multiprocessing must only be specified ' +
|
||||
'in a configuration file.\n' +
|
||||
f'Remove --{opt_name} from the command line.',
|
||||
)
|
||||
return ERR
|
||||
|
||||
os.environ["COVERAGE_RUN"] = "true"
|
||||
os.environ['COVERAGE_RUN'] = 'true'
|
||||
|
||||
runner = PyRunner(args, as_module=bool(options.module))
|
||||
runner.prepare()
|
||||
|
|
@ -870,28 +876,28 @@ class CoverageScript:
|
|||
"""Implementation of 'coverage debug'."""
|
||||
|
||||
if not args:
|
||||
show_help("What information would you like: config, data, sys, premain, pybehave?")
|
||||
show_help('What information would you like: config, data, sys, premain, pybehave?')
|
||||
return ERR
|
||||
if args[1:]:
|
||||
show_help("Only one topic at a time, please")
|
||||
show_help('Only one topic at a time, please')
|
||||
return ERR
|
||||
|
||||
if args[0] == "sys":
|
||||
write_formatted_info(print, "sys", self.coverage.sys_info())
|
||||
elif args[0] == "data":
|
||||
print(info_header("data"))
|
||||
if args[0] == 'sys':
|
||||
write_formatted_info(print, 'sys', self.coverage.sys_info())
|
||||
elif args[0] == 'data':
|
||||
print(info_header('data'))
|
||||
data_file = self.coverage.config.data_file
|
||||
debug_data_file(data_file)
|
||||
for filename in combinable_files(data_file):
|
||||
print("-----")
|
||||
print('-----')
|
||||
debug_data_file(filename)
|
||||
elif args[0] == "config":
|
||||
write_formatted_info(print, "config", self.coverage.config.debug_info())
|
||||
elif args[0] == "premain":
|
||||
print(info_header("premain"))
|
||||
elif args[0] == 'config':
|
||||
write_formatted_info(print, 'config', self.coverage.config.debug_info())
|
||||
elif args[0] == 'premain':
|
||||
print(info_header('premain'))
|
||||
print(short_stack(full=True))
|
||||
elif args[0] == "pybehave":
|
||||
write_formatted_info(print, "pybehave", env.debug_info())
|
||||
elif args[0] == 'pybehave':
|
||||
write_formatted_info(print, 'pybehave', env.debug_info())
|
||||
else:
|
||||
show_help(f"Don't know what you mean by {args[0]!r}")
|
||||
return ERR
|
||||
|
|
@ -910,7 +916,7 @@ def unshell_list(s: str) -> list[str] | None:
|
|||
# line, but (not) helpfully, the single quotes are included in the
|
||||
# argument, so we have to strip them off here.
|
||||
s = s.strip("'")
|
||||
return s.split(",")
|
||||
return s.split(',')
|
||||
|
||||
|
||||
def unglob_args(args: list[str]) -> list[str]:
|
||||
|
|
@ -918,7 +924,7 @@ def unglob_args(args: list[str]) -> list[str]:
|
|||
if env.WINDOWS:
|
||||
globbed = []
|
||||
for arg in args:
|
||||
if "?" in arg or "*" in arg:
|
||||
if '?' in arg or '*' in arg:
|
||||
globbed.extend(glob.glob(arg))
|
||||
else:
|
||||
globbed.append(arg)
|
||||
|
|
@ -927,7 +933,7 @@ def unglob_args(args: list[str]) -> list[str]:
|
|||
|
||||
|
||||
HELP_TOPICS = {
|
||||
"help": """\
|
||||
'help': """\
|
||||
Coverage.py, version {__version__} {extension_modifier}
|
||||
Measure, collect, and report on code coverage in Python programs.
|
||||
|
||||
|
|
@ -949,12 +955,12 @@ HELP_TOPICS = {
|
|||
Use "{program_name} help <command>" for detailed help on any command.
|
||||
""",
|
||||
|
||||
"minimum_help": (
|
||||
"Code coverage for Python, version {__version__} {extension_modifier}. " +
|
||||
'minimum_help': (
|
||||
'Code coverage for Python, version {__version__} {extension_modifier}. ' +
|
||||
"Use '{program_name} help' for help."
|
||||
),
|
||||
|
||||
"version": "Coverage.py, version {__version__} {extension_modifier}",
|
||||
'version': 'Coverage.py, version {__version__} {extension_modifier}',
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -987,11 +993,12 @@ def main(argv: list[str] | None = None) -> int | None:
|
|||
status = None
|
||||
return status
|
||||
|
||||
|
||||
# Profiling using ox_profile. Install it from GitHub:
|
||||
# pip install git+https://github.com/emin63/ox_profile.git
|
||||
#
|
||||
# $set_env.py: COVERAGE_PROFILE - Set to use ox_profile.
|
||||
_profile = os.getenv("COVERAGE_PROFILE")
|
||||
_profile = os.getenv('COVERAGE_PROFILE')
|
||||
if _profile: # pragma: debugging
|
||||
from ox_profile.core.launchers import SimpleLauncher # pylint: disable=import-error
|
||||
original_main = main
|
||||
|
|
@ -1004,6 +1011,6 @@ if _profile: # pragma: debugging
|
|||
try:
|
||||
return original_main(argv)
|
||||
finally:
|
||||
data, _ = profiler.query(re_filter="coverage", max_records=100)
|
||||
print(profiler.show(query=data, limit=100, sep="", col=""))
|
||||
data, _ = profiler.query(re_filter='coverage', max_records=100)
|
||||
print(profiler.show(query=data, limit=100, sep='', col=''))
|
||||
profiler.cancel()
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Raw data collector for coverage.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
|
||||
from types import FrameType
|
||||
from typing import (
|
||||
cast, Any, Callable, Dict, List, Mapping, Set, TypeVar,
|
||||
)
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Set
|
||||
from typing import TypeVar
|
||||
|
||||
from coverage import env
|
||||
from coverage.config import CoverageConfig
|
||||
|
|
@ -20,13 +22,17 @@ 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.misc import human_sorted_items
|
||||
from coverage.misc import 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,
|
||||
)
|
||||
from coverage.types import TArc
|
||||
from coverage.types import TFileDisposition
|
||||
from coverage.types import TracerCore
|
||||
from coverage.types import TTraceData
|
||||
from coverage.types import TTraceFn
|
||||
from coverage.types import TWarnFn
|
||||
|
||||
os = isolate_module(os)
|
||||
|
||||
|
|
@ -37,7 +43,7 @@ try:
|
|||
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
|
||||
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
|
||||
|
|
@ -48,7 +54,7 @@ except ImportError:
|
|||
sys.exit(1)
|
||||
HAS_CTRACER = False
|
||||
|
||||
T = TypeVar("T")
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class Collector:
|
||||
|
|
@ -73,7 +79,7 @@ class Collector:
|
|||
_collectors: list[Collector] = []
|
||||
|
||||
# The concurrency settings we support here.
|
||||
LIGHT_THREADS = {"greenlet", "eventlet", "gevent"}
|
||||
LIGHT_THREADS = {'greenlet', 'eventlet', 'gevent'}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -130,7 +136,7 @@ class Collector:
|
|||
self.branch = branch
|
||||
self.warn = warn
|
||||
self.concurrency = concurrency
|
||||
assert isinstance(self.concurrency, list), f"Expected a list: {self.concurrency!r}"
|
||||
assert isinstance(self.concurrency, list), f'Expected a list: {self.concurrency!r}'
|
||||
|
||||
self.pid = os.getpid()
|
||||
|
||||
|
|
@ -147,12 +153,12 @@ class Collector:
|
|||
|
||||
core: str | None
|
||||
if timid:
|
||||
core = "pytrace"
|
||||
core = 'pytrace'
|
||||
else:
|
||||
core = os.getenv("COVERAGE_CORE")
|
||||
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")
|
||||
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:
|
||||
|
|
@ -160,25 +166,25 @@ class Collector:
|
|||
# if env.PYBEHAVIOR.pep669 and self.should_start_context is None:
|
||||
# core = "sysmon"
|
||||
if HAS_CTRACER:
|
||||
core = "ctrace"
|
||||
core = 'ctrace'
|
||||
else:
|
||||
core = "pytrace"
|
||||
core = 'pytrace'
|
||||
|
||||
if core == "sysmon":
|
||||
if core == 'sysmon':
|
||||
self._trace_class = SysMonitor
|
||||
self._core_kwargs = {"tool_id": 3 if metacov else 1}
|
||||
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":
|
||||
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":
|
||||
elif core == 'pytrace':
|
||||
self._trace_class = PyTracer
|
||||
self._core_kwargs = {}
|
||||
self.file_disposition_class = FileDisposition
|
||||
|
|
@ -186,42 +192,42 @@ class Collector:
|
|||
self.packed_arcs = False
|
||||
self.systrace = True
|
||||
else:
|
||||
raise ConfigError(f"Unknown core value: {core!r}")
|
||||
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
|
||||
if unknown:
|
||||
show = ", ".join(sorted(unknown))
|
||||
raise ConfigError(f"Unknown concurrency choices: {show}")
|
||||
show = ', '.join(sorted(unknown))
|
||||
raise ConfigError(f'Unknown concurrency choices: {show}')
|
||||
light_threads = concurrencies & self.LIGHT_THREADS
|
||||
if len(light_threads) > 1:
|
||||
show = ", ".join(sorted(light_threads))
|
||||
raise ConfigError(f"Conflicting concurrency settings: {show}")
|
||||
show = ', '.join(sorted(light_threads))
|
||||
raise ConfigError(f'Conflicting concurrency settings: {show}')
|
||||
do_threading = False
|
||||
|
||||
tried = "nothing" # to satisfy pylint
|
||||
tried = 'nothing' # to satisfy pylint
|
||||
try:
|
||||
if "greenlet" in concurrencies:
|
||||
tried = "greenlet"
|
||||
if 'greenlet' in concurrencies:
|
||||
tried = 'greenlet'
|
||||
import greenlet
|
||||
self.concur_id_func = greenlet.getcurrent
|
||||
elif "eventlet" in concurrencies:
|
||||
tried = "eventlet"
|
||||
elif 'eventlet' in concurrencies:
|
||||
tried = 'eventlet'
|
||||
import eventlet.greenthread # pylint: disable=import-error,useless-suppression
|
||||
self.concur_id_func = eventlet.greenthread.getcurrent
|
||||
elif "gevent" in concurrencies:
|
||||
tried = "gevent"
|
||||
elif 'gevent' in concurrencies:
|
||||
tried = 'gevent'
|
||||
import gevent # pylint: disable=import-error,useless-suppression
|
||||
self.concur_id_func = gevent.getcurrent
|
||||
|
||||
if "thread" in concurrencies:
|
||||
if 'thread' in concurrencies:
|
||||
do_threading = True
|
||||
except ImportError as ex:
|
||||
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(self._trace_class, 'concur_id_func'):
|
||||
raise ConfigError(
|
||||
"Can't support concurrency={} with {}, only threads are supported.".format(
|
||||
tried, self.tracer_name(),
|
||||
|
|
@ -238,7 +244,7 @@ class Collector:
|
|||
self.reset()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Collector at {id(self):#x}: {self.tracer_name()}>"
|
||||
return f'<Collector at {id(self):#x}: {self.tracer_name()}>'
|
||||
|
||||
def use_data(self, covdata: CoverageData, context: str | None) -> None:
|
||||
"""Use `covdata` for recording data."""
|
||||
|
|
@ -296,7 +302,7 @@ class Collector:
|
|||
#
|
||||
# This gives a 20% benefit on the workload described at
|
||||
# https://bitbucket.org/pypy/pypy/issue/1871/10x-slower-than-cpython-under-coverage
|
||||
self.should_trace_cache = __pypy__.newdict("module")
|
||||
self.should_trace_cache = __pypy__.newdict('module')
|
||||
else:
|
||||
self.should_trace_cache = {}
|
||||
|
||||
|
|
@ -394,9 +400,9 @@ class Collector:
|
|||
"""Stop collecting trace information."""
|
||||
assert self._collectors
|
||||
if self._collectors[-1] is not self:
|
||||
print("self._collectors:")
|
||||
print('self._collectors:')
|
||||
for c in self._collectors:
|
||||
print(f" {c!r}\n{c.origin}")
|
||||
print(f' {c!r}\n{c.origin}')
|
||||
assert self._collectors[-1] is self, (
|
||||
f"Expected current collector to be {self!r}, but it's {self._collectors[-1]!r}"
|
||||
)
|
||||
|
|
@ -414,9 +420,9 @@ class Collector:
|
|||
tracer.stop()
|
||||
stats = tracer.get_stats()
|
||||
if stats:
|
||||
print("\nCoverage.py tracer stats:")
|
||||
print('\nCoverage.py tracer stats:')
|
||||
for k, v in human_sorted_items(stats.items()):
|
||||
print(f"{k:>20}: {v}")
|
||||
print(f'{k:>20}: {v}')
|
||||
if self.threading:
|
||||
self.threading.settrace(None)
|
||||
|
||||
|
|
@ -433,7 +439,7 @@ class Collector:
|
|||
def post_fork(self) -> None:
|
||||
"""After a fork, tracers might need to adjust."""
|
||||
for tracer in self.tracers:
|
||||
if hasattr(tracer, "post_fork"):
|
||||
if hasattr(tracer, 'post_fork'):
|
||||
tracer.post_fork()
|
||||
|
||||
def _activity(self) -> bool:
|
||||
|
|
@ -451,7 +457,7 @@ class Collector:
|
|||
if self.static_context:
|
||||
context = self.static_context
|
||||
if new_context:
|
||||
context += "|" + new_context
|
||||
context += '|' + new_context
|
||||
else:
|
||||
context = new_context
|
||||
self.covdata.set_context(context)
|
||||
|
|
@ -462,7 +468,7 @@ class Collector:
|
|||
assert file_tracer is not None
|
||||
plugin = file_tracer._coverage_plugin
|
||||
plugin_name = plugin._coverage_plugin_name
|
||||
self.warn(f"Disabling plug-in {plugin_name!r} due to previous exception")
|
||||
self.warn(f'Disabling plug-in {plugin_name!r} due to previous exception')
|
||||
plugin._coverage_enabled = False
|
||||
disposition.trace = False
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,30 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Config file for coverage.py"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import configparser
|
||||
import copy
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
|
||||
from typing import (
|
||||
Any, Callable, Iterable, Union,
|
||||
)
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Iterable
|
||||
from typing import Union
|
||||
|
||||
from coverage.exceptions import ConfigError
|
||||
from coverage.misc import isolate_module, human_sorted_items, substitute_variables
|
||||
from coverage.tomlconfig import TomlConfigParser, TomlDecodeError
|
||||
from coverage.types import (
|
||||
TConfigurable, TConfigSectionIn, TConfigValueIn, TConfigSectionOut,
|
||||
TConfigValueOut, TPluginConfig,
|
||||
)
|
||||
from coverage.misc import human_sorted_items
|
||||
from coverage.misc import isolate_module
|
||||
from coverage.misc import substitute_variables
|
||||
from coverage.tomlconfig import TomlConfigParser
|
||||
from coverage.tomlconfig import TomlDecodeError
|
||||
from coverage.types import TConfigSectionIn
|
||||
from coverage.types import TConfigSectionOut
|
||||
from coverage.types import TConfigurable
|
||||
from coverage.types import TConfigValueIn
|
||||
from coverage.types import TConfigValueOut
|
||||
from coverage.types import TPluginConfig
|
||||
|
||||
os = isolate_module(os)
|
||||
|
||||
|
|
@ -39,17 +41,17 @@ class HandyConfigParser(configparser.ConfigParser):
|
|||
"""
|
||||
|
||||
super().__init__(interpolation=None)
|
||||
self.section_prefixes = ["coverage:"]
|
||||
self.section_prefixes = ['coverage:']
|
||||
if our_file:
|
||||
self.section_prefixes.append("")
|
||||
self.section_prefixes.append('')
|
||||
|
||||
def read( # type: ignore[override]
|
||||
def read( # type: ignore[override]
|
||||
self,
|
||||
filenames: Iterable[str],
|
||||
encoding_unused: str | None = None,
|
||||
) -> list[str]:
|
||||
"""Read a file name as UTF-8 configuration data."""
|
||||
return super().read(filenames, encoding="utf-8")
|
||||
return super().read(filenames, encoding='utf-8')
|
||||
|
||||
def real_section(self, section: str) -> str | None:
|
||||
"""Get the actual name of a section."""
|
||||
|
|
@ -73,7 +75,7 @@ class HandyConfigParser(configparser.ConfigParser):
|
|||
real_section = self.real_section(section)
|
||||
if real_section is not None:
|
||||
return super().options(real_section)
|
||||
raise ConfigError(f"No section: {section!r}")
|
||||
raise ConfigError(f'No section: {section!r}')
|
||||
|
||||
def get_section(self, section: str) -> TConfigSectionOut:
|
||||
"""Get the contents of a section, as a dictionary."""
|
||||
|
|
@ -82,7 +84,7 @@ class HandyConfigParser(configparser.ConfigParser):
|
|||
d[opt] = self.get(section, opt)
|
||||
return d
|
||||
|
||||
def get(self, section: str, option: str, *args: Any, **kwargs: Any) -> str: # type: ignore
|
||||
def get(self, section: str, option: str, *args: Any, **kwargs: Any) -> str: # type: ignore
|
||||
"""Get a value, replacing environment variables also.
|
||||
|
||||
The arguments are the same as `ConfigParser.get`, but in the found
|
||||
|
|
@ -97,7 +99,7 @@ class HandyConfigParser(configparser.ConfigParser):
|
|||
if super().has_option(real_section, option):
|
||||
break
|
||||
else:
|
||||
raise ConfigError(f"No option {option!r} in section: {section!r}")
|
||||
raise ConfigError(f'No option {option!r} in section: {section!r}')
|
||||
|
||||
v: str = super().get(real_section, option, *args, **kwargs)
|
||||
v = substitute_variables(v, os.environ)
|
||||
|
|
@ -114,8 +116,8 @@ class HandyConfigParser(configparser.ConfigParser):
|
|||
"""
|
||||
value_list = self.get(section, option)
|
||||
values = []
|
||||
for value_line in value_list.split("\n"):
|
||||
for value in value_line.split(","):
|
||||
for value_line in value_list.split('\n'):
|
||||
for value in value_line.split(','):
|
||||
value = value.strip()
|
||||
if value:
|
||||
values.append(value)
|
||||
|
|
@ -138,7 +140,7 @@ class HandyConfigParser(configparser.ConfigParser):
|
|||
re.compile(value)
|
||||
except re.error as e:
|
||||
raise ConfigError(
|
||||
f"Invalid [{section}].{option} value {value!r}: {e}",
|
||||
f'Invalid [{section}].{option} value {value!r}: {e}',
|
||||
) from e
|
||||
if value:
|
||||
value_list.append(value)
|
||||
|
|
@ -150,20 +152,20 @@ TConfigParser = Union[HandyConfigParser, TomlConfigParser]
|
|||
|
||||
# The default line exclusion regexes.
|
||||
DEFAULT_EXCLUDE = [
|
||||
r"#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER)",
|
||||
r'#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER)',
|
||||
]
|
||||
|
||||
# The default partial branch regexes, to be modified by the user.
|
||||
DEFAULT_PARTIAL = [
|
||||
r"#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(branch|BRANCH)",
|
||||
r'#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(branch|BRANCH)',
|
||||
]
|
||||
|
||||
# The default partial branch regexes, based on Python semantics.
|
||||
# These are any Python branching constructs that can't actually execute all
|
||||
# their branches.
|
||||
DEFAULT_PARTIAL_ALWAYS = [
|
||||
"while (True|1|False|0):",
|
||||
"if (True|1|False|0):",
|
||||
'while (True|1|False|0):',
|
||||
'if (True|1|False|0):',
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -197,7 +199,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
|
|||
self.concurrency: list[str] = []
|
||||
self.context: str | None = None
|
||||
self.cover_pylib = False
|
||||
self.data_file = ".coverage"
|
||||
self.data_file = '.coverage'
|
||||
self.debug: list[str] = []
|
||||
self.debug_file: str | None = None
|
||||
self.disable_warnings: list[str] = []
|
||||
|
|
@ -233,23 +235,23 @@ class CoverageConfig(TConfigurable, TPluginConfig):
|
|||
|
||||
# Defaults for [html]
|
||||
self.extra_css: str | None = None
|
||||
self.html_dir = "htmlcov"
|
||||
self.html_dir = 'htmlcov'
|
||||
self.html_skip_covered: bool | None = None
|
||||
self.html_skip_empty: bool | None = None
|
||||
self.html_title = "Coverage report"
|
||||
self.html_title = 'Coverage report'
|
||||
self.show_contexts = False
|
||||
|
||||
# Defaults for [xml]
|
||||
self.xml_output = "coverage.xml"
|
||||
self.xml_output = 'coverage.xml'
|
||||
self.xml_package_depth = 99
|
||||
|
||||
# Defaults for [json]
|
||||
self.json_output = "coverage.json"
|
||||
self.json_output = 'coverage.json'
|
||||
self.json_pretty_print = False
|
||||
self.json_show_contexts = False
|
||||
|
||||
# Defaults for [lcov]
|
||||
self.lcov_output = "coverage.lcov"
|
||||
self.lcov_output = 'coverage.lcov'
|
||||
|
||||
# Defaults for [paths]
|
||||
self.paths: dict[str, list[str]] = {}
|
||||
|
|
@ -258,9 +260,9 @@ class CoverageConfig(TConfigurable, TPluginConfig):
|
|||
self.plugin_options: dict[str, TConfigSectionOut] = {}
|
||||
|
||||
MUST_BE_LIST = {
|
||||
"debug", "concurrency", "plugins",
|
||||
"report_omit", "report_include",
|
||||
"run_omit", "run_include",
|
||||
'debug', 'concurrency', 'plugins',
|
||||
'report_omit', 'report_include',
|
||||
'run_omit', 'run_include',
|
||||
}
|
||||
|
||||
def from_args(self, **kwargs: TConfigValueIn) -> None:
|
||||
|
|
@ -286,7 +288,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
|
|||
"""
|
||||
_, ext = os.path.splitext(filename)
|
||||
cp: TConfigParser
|
||||
if ext == ".toml":
|
||||
if ext == '.toml':
|
||||
cp = TomlConfigParser(our_file)
|
||||
else:
|
||||
cp = HandyConfigParser(our_file)
|
||||
|
|
@ -314,7 +316,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
|
|||
# Check that there are no unrecognized options.
|
||||
all_options = collections.defaultdict(set)
|
||||
for option_spec in self.CONFIG_FILE_OPTIONS:
|
||||
section, option = option_spec[1].split(":")
|
||||
section, option = option_spec[1].split(':')
|
||||
all_options[section].add(option)
|
||||
|
||||
for section, options in all_options.items():
|
||||
|
|
@ -328,9 +330,9 @@ class CoverageConfig(TConfigurable, TPluginConfig):
|
|||
)
|
||||
|
||||
# [paths] is special
|
||||
if cp.has_section("paths"):
|
||||
for option in cp.options("paths"):
|
||||
self.paths[option] = cp.getlist("paths", option)
|
||||
if cp.has_section('paths'):
|
||||
for option in cp.options('paths'):
|
||||
self.paths[option] = cp.getlist('paths', option)
|
||||
any_set = True
|
||||
|
||||
# plugins can have options
|
||||
|
|
@ -349,7 +351,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
|
|||
|
||||
if used:
|
||||
self.config_file = os.path.abspath(filename)
|
||||
with open(filename, "rb") as f:
|
||||
with open(filename, 'rb') as f:
|
||||
self._config_contents = f.read()
|
||||
|
||||
return used
|
||||
|
|
@ -358,7 +360,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
|
|||
"""Return a copy of the configuration."""
|
||||
return copy.deepcopy(self)
|
||||
|
||||
CONCURRENCY_CHOICES = {"thread", "gevent", "greenlet", "eventlet", "multiprocessing"}
|
||||
CONCURRENCY_CHOICES = {'thread', 'gevent', 'greenlet', 'eventlet', 'multiprocessing'}
|
||||
|
||||
CONFIG_FILE_OPTIONS = [
|
||||
# These are *args for _set_attr_from_config_option:
|
||||
|
|
@ -370,64 +372,64 @@ class CoverageConfig(TConfigurable, TPluginConfig):
|
|||
# configuration value from the file.
|
||||
|
||||
# [run]
|
||||
("branch", "run:branch", "boolean"),
|
||||
("command_line", "run:command_line"),
|
||||
("concurrency", "run:concurrency", "list"),
|
||||
("context", "run:context"),
|
||||
("cover_pylib", "run:cover_pylib", "boolean"),
|
||||
("data_file", "run:data_file"),
|
||||
("debug", "run:debug", "list"),
|
||||
("debug_file", "run:debug_file"),
|
||||
("disable_warnings", "run:disable_warnings", "list"),
|
||||
("dynamic_context", "run:dynamic_context"),
|
||||
("parallel", "run:parallel", "boolean"),
|
||||
("plugins", "run:plugins", "list"),
|
||||
("relative_files", "run:relative_files", "boolean"),
|
||||
("run_include", "run:include", "list"),
|
||||
("run_omit", "run:omit", "list"),
|
||||
("sigterm", "run:sigterm", "boolean"),
|
||||
("source", "run:source", "list"),
|
||||
("source_pkgs", "run:source_pkgs", "list"),
|
||||
("timid", "run:timid", "boolean"),
|
||||
("_crash", "run:_crash"),
|
||||
('branch', 'run:branch', 'boolean'),
|
||||
('command_line', 'run:command_line'),
|
||||
('concurrency', 'run:concurrency', 'list'),
|
||||
('context', 'run:context'),
|
||||
('cover_pylib', 'run:cover_pylib', 'boolean'),
|
||||
('data_file', 'run:data_file'),
|
||||
('debug', 'run:debug', 'list'),
|
||||
('debug_file', 'run:debug_file'),
|
||||
('disable_warnings', 'run:disable_warnings', 'list'),
|
||||
('dynamic_context', 'run:dynamic_context'),
|
||||
('parallel', 'run:parallel', 'boolean'),
|
||||
('plugins', 'run:plugins', 'list'),
|
||||
('relative_files', 'run:relative_files', 'boolean'),
|
||||
('run_include', 'run:include', 'list'),
|
||||
('run_omit', 'run:omit', 'list'),
|
||||
('sigterm', 'run:sigterm', 'boolean'),
|
||||
('source', 'run:source', 'list'),
|
||||
('source_pkgs', 'run:source_pkgs', 'list'),
|
||||
('timid', 'run:timid', 'boolean'),
|
||||
('_crash', 'run:_crash'),
|
||||
|
||||
# [report]
|
||||
("exclude_list", "report:exclude_lines", "regexlist"),
|
||||
("exclude_also", "report:exclude_also", "regexlist"),
|
||||
("fail_under", "report:fail_under", "float"),
|
||||
("format", "report:format"),
|
||||
("ignore_errors", "report:ignore_errors", "boolean"),
|
||||
("include_namespace_packages", "report:include_namespace_packages", "boolean"),
|
||||
("partial_always_list", "report:partial_branches_always", "regexlist"),
|
||||
("partial_list", "report:partial_branches", "regexlist"),
|
||||
("precision", "report:precision", "int"),
|
||||
("report_contexts", "report:contexts", "list"),
|
||||
("report_include", "report:include", "list"),
|
||||
("report_omit", "report:omit", "list"),
|
||||
("show_missing", "report:show_missing", "boolean"),
|
||||
("skip_covered", "report:skip_covered", "boolean"),
|
||||
("skip_empty", "report:skip_empty", "boolean"),
|
||||
("sort", "report:sort"),
|
||||
('exclude_list', 'report:exclude_lines', 'regexlist'),
|
||||
('exclude_also', 'report:exclude_also', 'regexlist'),
|
||||
('fail_under', 'report:fail_under', 'float'),
|
||||
('format', 'report:format'),
|
||||
('ignore_errors', 'report:ignore_errors', 'boolean'),
|
||||
('include_namespace_packages', 'report:include_namespace_packages', 'boolean'),
|
||||
('partial_always_list', 'report:partial_branches_always', 'regexlist'),
|
||||
('partial_list', 'report:partial_branches', 'regexlist'),
|
||||
('precision', 'report:precision', 'int'),
|
||||
('report_contexts', 'report:contexts', 'list'),
|
||||
('report_include', 'report:include', 'list'),
|
||||
('report_omit', 'report:omit', 'list'),
|
||||
('show_missing', 'report:show_missing', 'boolean'),
|
||||
('skip_covered', 'report:skip_covered', 'boolean'),
|
||||
('skip_empty', 'report:skip_empty', 'boolean'),
|
||||
('sort', 'report:sort'),
|
||||
|
||||
# [html]
|
||||
("extra_css", "html:extra_css"),
|
||||
("html_dir", "html:directory"),
|
||||
("html_skip_covered", "html:skip_covered", "boolean"),
|
||||
("html_skip_empty", "html:skip_empty", "boolean"),
|
||||
("html_title", "html:title"),
|
||||
("show_contexts", "html:show_contexts", "boolean"),
|
||||
('extra_css', 'html:extra_css'),
|
||||
('html_dir', 'html:directory'),
|
||||
('html_skip_covered', 'html:skip_covered', 'boolean'),
|
||||
('html_skip_empty', 'html:skip_empty', 'boolean'),
|
||||
('html_title', 'html:title'),
|
||||
('show_contexts', 'html:show_contexts', 'boolean'),
|
||||
|
||||
# [xml]
|
||||
("xml_output", "xml:output"),
|
||||
("xml_package_depth", "xml:package_depth", "int"),
|
||||
('xml_output', 'xml:output'),
|
||||
('xml_package_depth', 'xml:package_depth', 'int'),
|
||||
|
||||
# [json]
|
||||
("json_output", "json:output"),
|
||||
("json_pretty_print", "json:pretty_print", "boolean"),
|
||||
("json_show_contexts", "json:show_contexts", "boolean"),
|
||||
('json_output', 'json:output'),
|
||||
('json_pretty_print', 'json:pretty_print', 'boolean'),
|
||||
('json_show_contexts', 'json:show_contexts', 'boolean'),
|
||||
|
||||
# [lcov]
|
||||
("lcov_output", "lcov:output"),
|
||||
('lcov_output', 'lcov:output'),
|
||||
]
|
||||
|
||||
def _set_attr_from_config_option(
|
||||
|
|
@ -435,16 +437,16 @@ class CoverageConfig(TConfigurable, TPluginConfig):
|
|||
cp: TConfigParser,
|
||||
attr: str,
|
||||
where: str,
|
||||
type_: str = "",
|
||||
type_: str = '',
|
||||
) -> bool:
|
||||
"""Set an attribute on self if it exists in the ConfigParser.
|
||||
|
||||
Returns True if the attribute was set.
|
||||
|
||||
"""
|
||||
section, option = where.split(":")
|
||||
section, option = where.split(':')
|
||||
if cp.has_option(section, option):
|
||||
method = getattr(cp, "get" + type_)
|
||||
method = getattr(cp, 'get' + type_)
|
||||
setattr(self, attr, method(section, option))
|
||||
return True
|
||||
return False
|
||||
|
|
@ -464,7 +466,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
|
|||
|
||||
"""
|
||||
# Special-cased options.
|
||||
if option_name == "paths":
|
||||
if option_name == 'paths':
|
||||
self.paths = value # type: ignore[assignment]
|
||||
return
|
||||
|
||||
|
|
@ -476,13 +478,13 @@ class CoverageConfig(TConfigurable, TPluginConfig):
|
|||
return
|
||||
|
||||
# See if it's a plugin option.
|
||||
plugin_name, _, key = option_name.partition(":")
|
||||
plugin_name, _, key = option_name.partition(':')
|
||||
if key and plugin_name in self.plugins:
|
||||
self.plugin_options.setdefault(plugin_name, {})[key] = value # type: ignore[index]
|
||||
self.plugin_options.setdefault(plugin_name, {})[key] = value # type: ignore[index]
|
||||
return
|
||||
|
||||
# If we get here, we didn't find the option.
|
||||
raise ConfigError(f"No such option: {option_name!r}")
|
||||
raise ConfigError(f'No such option: {option_name!r}')
|
||||
|
||||
def get_option(self, option_name: str) -> TConfigValueOut | None:
|
||||
"""Get an option from the configuration.
|
||||
|
|
@ -495,7 +497,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
|
|||
|
||||
"""
|
||||
# Special-cased options.
|
||||
if option_name == "paths":
|
||||
if option_name == 'paths':
|
||||
return self.paths # type: ignore[return-value]
|
||||
|
||||
# Check all the hard-coded options.
|
||||
|
|
@ -505,12 +507,12 @@ class CoverageConfig(TConfigurable, TPluginConfig):
|
|||
return getattr(self, attr) # type: ignore[no-any-return]
|
||||
|
||||
# See if it's a plugin option.
|
||||
plugin_name, _, key = option_name.partition(":")
|
||||
plugin_name, _, key = option_name.partition(':')
|
||||
if key and plugin_name in self.plugins:
|
||||
return self.plugin_options.get(plugin_name, {}).get(key)
|
||||
|
||||
# If we get here, we didn't find the option.
|
||||
raise ConfigError(f"No such option: {option_name!r}")
|
||||
raise ConfigError(f'No such option: {option_name!r}')
|
||||
|
||||
def post_process_file(self, path: str) -> str:
|
||||
"""Make final adjustments to a file path to make it usable."""
|
||||
|
|
@ -530,7 +532,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
|
|||
def debug_info(self) -> list[tuple[str, Any]]:
|
||||
"""Make a list of (name, value) pairs for writing debug info."""
|
||||
return human_sorted_items(
|
||||
(k, v) for k, v in self.__dict__.items() if not k.startswith("_")
|
||||
(k, v) for k, v in self.__dict__.items() if not k.startswith('_')
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -543,24 +545,24 @@ def config_files_to_try(config_file: bool | str) -> list[tuple[str, bool, bool]]
|
|||
|
||||
# Some API users were specifying ".coveragerc" to mean the same as
|
||||
# True, so make it so.
|
||||
if config_file == ".coveragerc":
|
||||
if config_file == '.coveragerc':
|
||||
config_file = True
|
||||
specified_file = (config_file is not True)
|
||||
if not specified_file:
|
||||
# No file was specified. Check COVERAGE_RCFILE.
|
||||
rcfile = os.getenv("COVERAGE_RCFILE")
|
||||
rcfile = os.getenv('COVERAGE_RCFILE')
|
||||
if rcfile:
|
||||
config_file = rcfile
|
||||
specified_file = True
|
||||
if not specified_file:
|
||||
# Still no file specified. Default to .coveragerc
|
||||
config_file = ".coveragerc"
|
||||
config_file = '.coveragerc'
|
||||
assert isinstance(config_file, str)
|
||||
files_to_try = [
|
||||
(config_file, True, specified_file),
|
||||
("setup.cfg", False, False),
|
||||
("tox.ini", False, False),
|
||||
("pyproject.toml", False, False),
|
||||
('setup.cfg', False, False),
|
||||
('tox.ini', False, False),
|
||||
('pyproject.toml', False, False),
|
||||
]
|
||||
return files_to_try
|
||||
|
||||
|
|
@ -601,13 +603,13 @@ def read_coverage_config(
|
|||
raise ConfigError(f"Couldn't read {fname!r} as a config file")
|
||||
|
||||
# 3) from environment variables:
|
||||
env_data_file = os.getenv("COVERAGE_FILE")
|
||||
env_data_file = os.getenv('COVERAGE_FILE')
|
||||
if env_data_file:
|
||||
config.data_file = env_data_file
|
||||
# $set_env.py: COVERAGE_DEBUG - Debug options: https://coverage.rtfd.io/cmd.html#debug
|
||||
debugs = os.getenv("COVERAGE_DEBUG")
|
||||
debugs = os.getenv('COVERAGE_DEBUG')
|
||||
if debugs:
|
||||
config.debug.extend(d.strip() for d in debugs.split(","))
|
||||
config.debug.extend(d.strip() for d in debugs.split(','))
|
||||
|
||||
# 4) from constructor arguments:
|
||||
config.from_args(**kwargs)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Determine contexts for coverage.py"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import FrameType
|
||||
from typing import cast, Callable, Sequence
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Sequence
|
||||
|
||||
|
||||
def combine_context_switchers(
|
||||
|
|
@ -44,7 +44,7 @@ def combine_context_switchers(
|
|||
def should_start_context_test_function(frame: FrameType) -> str | None:
|
||||
"""Is this frame calling a test_* function?"""
|
||||
co_name = frame.f_code.co_name
|
||||
if co_name.startswith("test") or co_name == "runTest":
|
||||
if co_name.startswith('test') or co_name == 'runTest':
|
||||
return qualname_from_frame(frame)
|
||||
return None
|
||||
|
||||
|
|
@ -54,19 +54,19 @@ def qualname_from_frame(frame: FrameType) -> str | None:
|
|||
co = frame.f_code
|
||||
fname = co.co_name
|
||||
method = None
|
||||
if co.co_argcount and co.co_varnames[0] == "self":
|
||||
self = frame.f_locals.get("self", None)
|
||||
if co.co_argcount and co.co_varnames[0] == 'self':
|
||||
self = frame.f_locals.get('self', None)
|
||||
method = getattr(self, fname, None)
|
||||
|
||||
if method is None:
|
||||
func = frame.f_globals.get(fname)
|
||||
if func is None:
|
||||
return None
|
||||
return cast(str, func.__module__ + "." + fname)
|
||||
return cast(str, func.__module__ + '.' + fname)
|
||||
|
||||
func = getattr(method, "__func__", None)
|
||||
func = getattr(method, '__func__', None)
|
||||
if func is None:
|
||||
cls = self.__class__
|
||||
return cast(str, cls.__module__ + "." + cls.__name__ + "." + fname)
|
||||
return cast(str, cls.__module__ + '.' + cls.__name__ + '.' + fname)
|
||||
|
||||
return cast(str, func.__module__ + "." + func.__qualname__)
|
||||
return cast(str, func.__module__ + '.' + func.__qualname__)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Central control stuff for coverage.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
import collections
|
||||
import contextlib
|
||||
import os
|
||||
import os.path
|
||||
import platform
|
||||
import signal
|
||||
|
|
@ -16,31 +13,48 @@ import sys
|
|||
import threading
|
||||
import time
|
||||
import warnings
|
||||
|
||||
from types import FrameType
|
||||
from typing import (
|
||||
cast,
|
||||
Any, Callable, IO, Iterable, Iterator, List,
|
||||
)
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import IO
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
|
||||
from coverage import env
|
||||
from coverage.annotate import AnnotateReporter
|
||||
from coverage.collector import Collector, HAS_CTRACER
|
||||
from coverage.config import CoverageConfig, read_coverage_config
|
||||
from coverage.context import should_start_context_test_function, combine_context_switchers
|
||||
from coverage.data import CoverageData, combine_parallel_data
|
||||
from coverage.debug import (
|
||||
DebugControl, NoDebugging, short_stack, write_formatted_info, relevant_environment_display,
|
||||
)
|
||||
from coverage.collector import Collector
|
||||
from coverage.collector import HAS_CTRACER
|
||||
from coverage.config import CoverageConfig
|
||||
from coverage.config import read_coverage_config
|
||||
from coverage.context import combine_context_switchers
|
||||
from coverage.context import should_start_context_test_function
|
||||
from coverage.data import combine_parallel_data
|
||||
from coverage.data import CoverageData
|
||||
from coverage.debug import DebugControl
|
||||
from coverage.debug import NoDebugging
|
||||
from coverage.debug import relevant_environment_display
|
||||
from coverage.debug import short_stack
|
||||
from coverage.debug import write_formatted_info
|
||||
from coverage.disposition import disposition_debug_msg
|
||||
from coverage.exceptions import ConfigError, CoverageException, CoverageWarning, PluginError
|
||||
from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory
|
||||
from coverage.exceptions import ConfigError
|
||||
from coverage.exceptions import CoverageException
|
||||
from coverage.exceptions import CoverageWarning
|
||||
from coverage.exceptions import PluginError
|
||||
from coverage.files import abs_file
|
||||
from coverage.files import PathAliases
|
||||
from coverage.files import relative_filename
|
||||
from coverage.files import set_relative_directory
|
||||
from coverage.html import HtmlReporter
|
||||
from coverage.inorout import InOrOut
|
||||
from coverage.jsonreport import JsonReporter
|
||||
from coverage.lcovreport import LcovReporter
|
||||
from coverage.misc import bool_or_none, join_regex
|
||||
from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module
|
||||
from coverage.misc import bool_or_none
|
||||
from coverage.misc import DefaultValue
|
||||
from coverage.misc import ensure_dir_for_file
|
||||
from coverage.misc import isolate_module
|
||||
from coverage.misc import join_regex
|
||||
from coverage.multiproc import patch_multiprocessing
|
||||
from coverage.plugin import FileReporter
|
||||
from coverage.plugin_support import Plugins
|
||||
|
|
@ -48,14 +62,19 @@ from coverage.python import PythonFileReporter
|
|||
from coverage.report import SummaryReporter
|
||||
from coverage.report_core import render_report
|
||||
from coverage.results import Analysis
|
||||
from coverage.types import (
|
||||
FilePath, TConfigurable, TConfigSectionIn, TConfigValueIn, TConfigValueOut,
|
||||
TFileDisposition, TLineNo, TMorf,
|
||||
)
|
||||
from coverage.types import FilePath
|
||||
from coverage.types import TConfigSectionIn
|
||||
from coverage.types import TConfigurable
|
||||
from coverage.types import TConfigValueIn
|
||||
from coverage.types import TConfigValueOut
|
||||
from coverage.types import TFileDisposition
|
||||
from coverage.types import TLineNo
|
||||
from coverage.types import TMorf
|
||||
from coverage.xmlreport import XmlReporter
|
||||
|
||||
os = isolate_module(os)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def override_config(cov: Coverage, **kwargs: TConfigValueIn) -> Iterator[None]:
|
||||
"""Temporarily tweak the configuration of `cov`.
|
||||
|
|
@ -72,9 +91,10 @@ def override_config(cov: Coverage, **kwargs: TConfigValueIn) -> Iterator[None]:
|
|||
cov.config = original_config
|
||||
|
||||
|
||||
DEFAULT_DATAFILE = DefaultValue("MISSING")
|
||||
DEFAULT_DATAFILE = DefaultValue('MISSING')
|
||||
_DEFAULT_DATAFILE = DEFAULT_DATAFILE # Just in case, for backwards compatibility
|
||||
|
||||
|
||||
class Coverage(TConfigurable):
|
||||
"""Programmatic access to coverage.py.
|
||||
|
||||
|
|
@ -323,10 +343,10 @@ class Coverage(TConfigurable):
|
|||
|
||||
# Create and configure the debugging controller.
|
||||
self._debug = DebugControl(self.config.debug, self._debug_file, self.config.debug_file)
|
||||
if self._debug.should("process"):
|
||||
self._debug.write("Coverage._init")
|
||||
if self._debug.should('process'):
|
||||
self._debug.write('Coverage._init')
|
||||
|
||||
if "multiprocessing" in (self.config.concurrency or ()):
|
||||
if 'multiprocessing' in (self.config.concurrency or ()):
|
||||
# Multi-processing uses parallel for the subprocesses, so also use
|
||||
# it for the main process.
|
||||
self.config.parallel = True
|
||||
|
|
@ -358,31 +378,31 @@ class Coverage(TConfigurable):
|
|||
# "[run] _crash" will raise an exception if the value is close by in
|
||||
# the call stack, for testing error handling.
|
||||
if self.config._crash and self.config._crash in short_stack():
|
||||
raise RuntimeError(f"Crashing because called by {self.config._crash}")
|
||||
raise RuntimeError(f'Crashing because called by {self.config._crash}')
|
||||
|
||||
def _write_startup_debug(self) -> None:
|
||||
"""Write out debug info at startup if needed."""
|
||||
wrote_any = False
|
||||
with self._debug.without_callers():
|
||||
if self._debug.should("config"):
|
||||
if self._debug.should('config'):
|
||||
config_info = self.config.debug_info()
|
||||
write_formatted_info(self._debug.write, "config", config_info)
|
||||
write_formatted_info(self._debug.write, 'config', config_info)
|
||||
wrote_any = True
|
||||
|
||||
if self._debug.should("sys"):
|
||||
write_formatted_info(self._debug.write, "sys", self.sys_info())
|
||||
if self._debug.should('sys'):
|
||||
write_formatted_info(self._debug.write, 'sys', self.sys_info())
|
||||
for plugin in self._plugins:
|
||||
header = "sys: " + plugin._coverage_plugin_name
|
||||
header = 'sys: ' + plugin._coverage_plugin_name
|
||||
info = plugin.sys_info()
|
||||
write_formatted_info(self._debug.write, header, info)
|
||||
wrote_any = True
|
||||
|
||||
if self._debug.should("pybehave"):
|
||||
write_formatted_info(self._debug.write, "pybehave", env.debug_info())
|
||||
if self._debug.should('pybehave'):
|
||||
write_formatted_info(self._debug.write, 'pybehave', env.debug_info())
|
||||
wrote_any = True
|
||||
|
||||
if wrote_any:
|
||||
write_formatted_info(self._debug.write, "end", ())
|
||||
write_formatted_info(self._debug.write, 'end', ())
|
||||
|
||||
def _should_trace(self, filename: str, frame: FrameType) -> TFileDisposition:
|
||||
"""Decide whether to trace execution in `filename`.
|
||||
|
|
@ -392,7 +412,7 @@ class Coverage(TConfigurable):
|
|||
"""
|
||||
assert self._inorout is not None
|
||||
disp = self._inorout.should_trace(filename, frame)
|
||||
if self._debug.should("trace"):
|
||||
if self._debug.should('trace'):
|
||||
self._debug.write(disposition_debug_msg(disp))
|
||||
return disp
|
||||
|
||||
|
|
@ -404,11 +424,11 @@ class Coverage(TConfigurable):
|
|||
"""
|
||||
assert self._inorout is not None
|
||||
reason = self._inorout.check_include_omit_etc(filename, frame)
|
||||
if self._debug.should("trace"):
|
||||
if self._debug.should('trace'):
|
||||
if not reason:
|
||||
msg = f"Including {filename!r}"
|
||||
msg = f'Including {filename!r}'
|
||||
else:
|
||||
msg = f"Not including {filename!r}: {reason}"
|
||||
msg = f'Not including {filename!r}: {reason}'
|
||||
self._debug.write(msg)
|
||||
|
||||
return not reason
|
||||
|
|
@ -431,9 +451,9 @@ class Coverage(TConfigurable):
|
|||
|
||||
self._warnings.append(msg)
|
||||
if slug:
|
||||
msg = f"{msg} ({slug})"
|
||||
if self._debug.should("pid"):
|
||||
msg = f"[{os.getpid()}] {msg}"
|
||||
msg = f'{msg} ({slug})'
|
||||
if self._debug.should('pid'):
|
||||
msg = f'[{os.getpid()}] {msg}'
|
||||
warnings.warn(msg, category=CoverageWarning, stacklevel=2)
|
||||
|
||||
if once:
|
||||
|
|
@ -512,15 +532,15 @@ class Coverage(TConfigurable):
|
|||
"""Initialization for start()"""
|
||||
# Construct the collector.
|
||||
concurrency: list[str] = self.config.concurrency or []
|
||||
if "multiprocessing" in concurrency:
|
||||
if 'multiprocessing' in concurrency:
|
||||
if self.config.config_file is None:
|
||||
raise ConfigError("multiprocessing requires a configuration file")
|
||||
raise ConfigError('multiprocessing requires a configuration file')
|
||||
patch_multiprocessing(rcfile=self.config.config_file)
|
||||
|
||||
dycon = self.config.dynamic_context
|
||||
if not dycon or dycon == "none":
|
||||
if not dycon or dycon == 'none':
|
||||
context_switchers = []
|
||||
elif dycon == "test_function":
|
||||
elif dycon == 'test_function':
|
||||
context_switchers = [should_start_context_test_function]
|
||||
else:
|
||||
raise ConfigError(f"Don't understand dynamic_context setting: {dycon!r}")
|
||||
|
|
@ -565,9 +585,9 @@ class Coverage(TConfigurable):
|
|||
if self._plugins.file_tracers and not self._collector.supports_plugins:
|
||||
self._warn(
|
||||
"Plugin file tracers ({}) aren't supported with {}".format(
|
||||
", ".join(
|
||||
', '.join(
|
||||
plugin._coverage_plugin_name
|
||||
for plugin in self._plugins.file_tracers
|
||||
for plugin in self._plugins.file_tracers
|
||||
),
|
||||
self._collector.tracer_name(),
|
||||
),
|
||||
|
|
@ -579,7 +599,7 @@ class Coverage(TConfigurable):
|
|||
self._inorout = InOrOut(
|
||||
config=self.config,
|
||||
warn=self._warn,
|
||||
debug=(self._debug if self._debug.should("trace") else None),
|
||||
debug=(self._debug if self._debug.should('trace') else None),
|
||||
include_namespace_packages=self.config.include_namespace_packages,
|
||||
)
|
||||
self._inorout.plugins = self._plugins
|
||||
|
|
@ -676,18 +696,18 @@ class Coverage(TConfigurable):
|
|||
finally:
|
||||
self.stop()
|
||||
|
||||
def _atexit(self, event: str = "atexit") -> None:
|
||||
def _atexit(self, event: str = 'atexit') -> None:
|
||||
"""Clean up on process shutdown."""
|
||||
if self._debug.should("process"):
|
||||
self._debug.write(f"{event}: pid: {os.getpid()}, instance: {self!r}")
|
||||
if self._debug.should('process'):
|
||||
self._debug.write(f'{event}: pid: {os.getpid()}, instance: {self!r}')
|
||||
if self._started:
|
||||
self.stop()
|
||||
if self._auto_save or event == "sigterm":
|
||||
if self._auto_save or event == 'sigterm':
|
||||
self.save()
|
||||
|
||||
def _on_sigterm(self, signum_unused: int, frame_unused: FrameType | None) -> None:
|
||||
"""A handler for signal.SIGTERM."""
|
||||
self._atexit("sigterm")
|
||||
self._atexit('sigterm')
|
||||
# Statements after here won't be seen by metacov because we just wrote
|
||||
# the data, and are about to kill the process.
|
||||
signal.signal(signal.SIGTERM, self._old_sigterm) # pragma: not covered
|
||||
|
|
@ -724,21 +744,21 @@ class Coverage(TConfigurable):
|
|||
|
||||
"""
|
||||
if not self._started: # pragma: part started
|
||||
raise CoverageException("Cannot switch context, coverage is not started")
|
||||
raise CoverageException('Cannot switch context, coverage is not started')
|
||||
|
||||
assert self._collector is not None
|
||||
if self._collector.should_start_context:
|
||||
self._warn("Conflicting dynamic contexts", slug="dynamic-conflict", once=True)
|
||||
self._warn('Conflicting dynamic contexts', slug='dynamic-conflict', once=True)
|
||||
|
||||
self._collector.switch_context(new_context)
|
||||
|
||||
def clear_exclude(self, which: str = "exclude") -> None:
|
||||
def clear_exclude(self, which: str = 'exclude') -> None:
|
||||
"""Clear the exclude list."""
|
||||
self._init()
|
||||
setattr(self.config, which + "_list", [])
|
||||
setattr(self.config, which + '_list', [])
|
||||
self._exclude_regex_stale()
|
||||
|
||||
def exclude(self, regex: str, which: str = "exclude") -> None:
|
||||
def exclude(self, regex: str, which: str = 'exclude') -> None:
|
||||
"""Exclude source lines from execution consideration.
|
||||
|
||||
A number of lists of regular expressions are maintained. Each list
|
||||
|
|
@ -754,7 +774,7 @@ class Coverage(TConfigurable):
|
|||
|
||||
"""
|
||||
self._init()
|
||||
excl_list = getattr(self.config, which + "_list")
|
||||
excl_list = getattr(self.config, which + '_list')
|
||||
excl_list.append(regex)
|
||||
self._exclude_regex_stale()
|
||||
|
||||
|
|
@ -765,11 +785,11 @@ class Coverage(TConfigurable):
|
|||
def _exclude_regex(self, which: str) -> str:
|
||||
"""Return a regex string for the given exclusion list."""
|
||||
if which not in self._exclude_re:
|
||||
excl_list = getattr(self.config, which + "_list")
|
||||
excl_list = getattr(self.config, which + '_list')
|
||||
self._exclude_re[which] = join_regex(excl_list)
|
||||
return self._exclude_re[which]
|
||||
|
||||
def get_exclude_list(self, which: str = "exclude") -> list[str]:
|
||||
def get_exclude_list(self, which: str = 'exclude') -> list[str]:
|
||||
"""Return a list of excluded regex strings.
|
||||
|
||||
`which` indicates which list is desired. See :meth:`exclude` for the
|
||||
|
|
@ -777,7 +797,7 @@ class Coverage(TConfigurable):
|
|||
|
||||
"""
|
||||
self._init()
|
||||
return cast(List[str], getattr(self.config, which + "_list"))
|
||||
return cast(List[str], getattr(self.config, which + '_list'))
|
||||
|
||||
def save(self) -> None:
|
||||
"""Save the collected coverage data to the data file."""
|
||||
|
|
@ -787,7 +807,7 @@ class Coverage(TConfigurable):
|
|||
def _make_aliases(self) -> PathAliases:
|
||||
"""Create a PathAliases from our configuration."""
|
||||
aliases = PathAliases(
|
||||
debugfn=(self._debug.write if self._debug.should("pathmap") else None),
|
||||
debugfn=(self._debug.write if self._debug.should('pathmap') else None),
|
||||
relative=self.config.relative_files,
|
||||
)
|
||||
for paths in self.config.paths.values():
|
||||
|
|
@ -884,7 +904,7 @@ class Coverage(TConfigurable):
|
|||
|
||||
# Find out if we got any data.
|
||||
if not self._data and self._warn_no_data:
|
||||
self._warn("No data was collected.", slug="no-data-collected")
|
||||
self._warn('No data was collected.', slug='no-data-collected')
|
||||
|
||||
# Touch all the files that could have executed, so that we can
|
||||
# mark completely un-executed files as 0% covered.
|
||||
|
|
@ -952,7 +972,7 @@ class Coverage(TConfigurable):
|
|||
"""Get a FileReporter for a module or file name."""
|
||||
assert self._data is not None
|
||||
plugin = None
|
||||
file_reporter: str | FileReporter = "python"
|
||||
file_reporter: str | FileReporter = 'python'
|
||||
|
||||
if isinstance(morf, str):
|
||||
mapped_morf = self._file_mapper(morf)
|
||||
|
|
@ -964,12 +984,12 @@ class Coverage(TConfigurable):
|
|||
file_reporter = plugin.file_reporter(mapped_morf)
|
||||
if file_reporter is None:
|
||||
raise PluginError(
|
||||
"Plugin {!r} did not provide a file reporter for {!r}.".format(
|
||||
'Plugin {!r} did not provide a file reporter for {!r}.'.format(
|
||||
plugin._coverage_plugin_name, morf,
|
||||
),
|
||||
)
|
||||
|
||||
if file_reporter == "python":
|
||||
if file_reporter == 'python':
|
||||
file_reporter = PythonFileReporter(morf, self)
|
||||
|
||||
assert isinstance(file_reporter, FileReporter)
|
||||
|
|
@ -1290,36 +1310,37 @@ class Coverage(TConfigurable):
|
|||
for plugin in plugins:
|
||||
entry = plugin._coverage_plugin_name
|
||||
if not plugin._coverage_enabled:
|
||||
entry += " (disabled)"
|
||||
entry += ' (disabled)'
|
||||
entries.append(entry)
|
||||
return entries
|
||||
|
||||
info = [
|
||||
("coverage_version", covmod.__version__),
|
||||
("coverage_module", covmod.__file__),
|
||||
("core", self._collector.tracer_name() if self._collector is not None else "-none-"),
|
||||
("CTracer", "available" if HAS_CTRACER else "unavailable"),
|
||||
("plugins.file_tracers", plugin_info(self._plugins.file_tracers)),
|
||||
("plugins.configurers", plugin_info(self._plugins.configurers)),
|
||||
("plugins.context_switchers", plugin_info(self._plugins.context_switchers)),
|
||||
("configs_attempted", self.config.attempted_config_files),
|
||||
("configs_read", self.config.config_files_read),
|
||||
("config_file", self.config.config_file),
|
||||
("config_contents",
|
||||
repr(self.config._config_contents) if self.config._config_contents else "-none-",
|
||||
('coverage_version', covmod.__version__),
|
||||
('coverage_module', covmod.__file__),
|
||||
('core', self._collector.tracer_name() if self._collector is not None else '-none-'),
|
||||
('CTracer', 'available' if HAS_CTRACER else 'unavailable'),
|
||||
('plugins.file_tracers', plugin_info(self._plugins.file_tracers)),
|
||||
('plugins.configurers', plugin_info(self._plugins.configurers)),
|
||||
('plugins.context_switchers', plugin_info(self._plugins.context_switchers)),
|
||||
('configs_attempted', self.config.attempted_config_files),
|
||||
('configs_read', self.config.config_files_read),
|
||||
('config_file', self.config.config_file),
|
||||
(
|
||||
'config_contents',
|
||||
repr(self.config._config_contents) if self.config._config_contents else '-none-',
|
||||
),
|
||||
("data_file", self._data.data_filename() if self._data is not None else "-none-"),
|
||||
("python", sys.version.replace("\n", "")),
|
||||
("platform", platform.platform()),
|
||||
("implementation", platform.python_implementation()),
|
||||
("executable", sys.executable),
|
||||
("def_encoding", sys.getdefaultencoding()),
|
||||
("fs_encoding", sys.getfilesystemencoding()),
|
||||
("pid", os.getpid()),
|
||||
("cwd", os.getcwd()),
|
||||
("path", sys.path),
|
||||
("environment", [f"{k} = {v}" for k, v in relevant_environment_display(os.environ)]),
|
||||
("command_line", " ".join(getattr(sys, "argv", ["-none-"]))),
|
||||
('data_file', self._data.data_filename() if self._data is not None else '-none-'),
|
||||
('python', sys.version.replace('\n', '')),
|
||||
('platform', platform.platform()),
|
||||
('implementation', platform.python_implementation()),
|
||||
('executable', sys.executable),
|
||||
('def_encoding', sys.getdefaultencoding()),
|
||||
('fs_encoding', sys.getfilesystemencoding()),
|
||||
('pid', os.getpid()),
|
||||
('cwd', os.getcwd()),
|
||||
('path', sys.path),
|
||||
('environment', [f'{k} = {v}' for k, v in relevant_environment_display(os.environ)]),
|
||||
('command_line', ' '.join(getattr(sys, 'argv', ['-none-']))),
|
||||
]
|
||||
|
||||
if self._inorout is not None:
|
||||
|
|
@ -1332,12 +1353,12 @@ class Coverage(TConfigurable):
|
|||
|
||||
# Mega debugging...
|
||||
# $set_env.py: COVERAGE_DEBUG_CALLS - Lots and lots of output about calls to Coverage.
|
||||
if int(os.getenv("COVERAGE_DEBUG_CALLS", 0)): # pragma: debugging
|
||||
if int(os.getenv('COVERAGE_DEBUG_CALLS', 0)): # pragma: debugging
|
||||
from coverage.debug import decorate_methods, show_calls
|
||||
|
||||
Coverage = decorate_methods( # type: ignore[misc]
|
||||
show_calls(show_args=True),
|
||||
butnot=["get_data"],
|
||||
butnot=['get_data'],
|
||||
)(Coverage)
|
||||
|
||||
|
||||
|
|
@ -1364,7 +1385,7 @@ def process_startup() -> Coverage | None:
|
|||
not started by this call.
|
||||
|
||||
"""
|
||||
cps = os.getenv("COVERAGE_PROCESS_START")
|
||||
cps = os.getenv('COVERAGE_PROCESS_START')
|
||||
if not cps:
|
||||
# No request for coverage, nothing to do.
|
||||
return None
|
||||
|
|
@ -1378,7 +1399,7 @@ def process_startup() -> Coverage | None:
|
|||
#
|
||||
# https://github.com/nedbat/coveragepy/issues/340 has more details.
|
||||
|
||||
if hasattr(process_startup, "coverage"):
|
||||
if hasattr(process_startup, 'coverage'):
|
||||
# We've annotated this function before, so we must have already
|
||||
# started coverage.py in this process. Nothing to do.
|
||||
return None
|
||||
|
|
@ -1396,6 +1417,6 @@ def process_startup() -> Coverage | None:
|
|||
|
||||
def _prevent_sub_process_measurement() -> None:
|
||||
"""Stop any subprocess auto-measurement from writing data."""
|
||||
auto_created_coverage = getattr(process_startup, "coverage", None)
|
||||
auto_created_coverage = getattr(process_startup, 'coverage', None)
|
||||
if auto_created_coverage is not None:
|
||||
auto_created_coverage._auto_save = False
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Coverage data for coverage.py.
|
||||
|
||||
This file had the 4.x JSON data support, which is now gone. This file still
|
||||
|
|
@ -9,18 +8,21 @@ CoverageData is now defined in sqldata.py, and imported here to keep the
|
|||
imports working.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import hashlib
|
||||
import os.path
|
||||
from typing import Callable
|
||||
from typing import Iterable
|
||||
|
||||
from typing import Callable, Iterable
|
||||
|
||||
from coverage.exceptions import CoverageException, NoDataError
|
||||
from coverage.exceptions import CoverageException
|
||||
from coverage.exceptions import NoDataError
|
||||
from coverage.files import PathAliases
|
||||
from coverage.misc import Hasher, file_be_gone, human_sorted, plural
|
||||
from coverage.misc import file_be_gone
|
||||
from coverage.misc import Hasher
|
||||
from coverage.misc import human_sorted
|
||||
from coverage.misc import plural
|
||||
from coverage.sqldata import CoverageData
|
||||
|
||||
|
||||
|
|
@ -38,7 +40,7 @@ def line_counts(data: CoverageData, fullpath: bool = False) -> dict[str, int]:
|
|||
filename_fn: Callable[[str], str]
|
||||
if fullpath:
|
||||
# pylint: disable=unnecessary-lambda-assignment
|
||||
filename_fn = lambda f: f
|
||||
def filename_fn(f): return f
|
||||
else:
|
||||
filename_fn = os.path.basename
|
||||
for filename in data.measured_files():
|
||||
|
|
@ -79,14 +81,14 @@ def combinable_files(data_file: str, data_paths: Iterable[str] | None = None) ->
|
|||
if os.path.isfile(p):
|
||||
files_to_combine.append(os.path.abspath(p))
|
||||
elif os.path.isdir(p):
|
||||
pattern = glob.escape(os.path.join(os.path.abspath(p), local)) +".*"
|
||||
pattern = glob.escape(os.path.join(os.path.abspath(p), local)) + '.*'
|
||||
files_to_combine.extend(glob.glob(pattern))
|
||||
else:
|
||||
raise NoDataError(f"Couldn't combine from non-existent path '{p}'")
|
||||
|
||||
# SQLite might have made journal files alongside our database files.
|
||||
# We never want to combine those.
|
||||
files_to_combine = [fnm for fnm in files_to_combine if not fnm.endswith("-journal")]
|
||||
files_to_combine = [fnm for fnm in files_to_combine if not fnm.endswith('-journal')]
|
||||
|
||||
# Sorting isn't usually needed, since it shouldn't matter what order files
|
||||
# are combined, but sorting makes tests more predictable, and makes
|
||||
|
|
@ -132,7 +134,7 @@ def combine_parallel_data(
|
|||
files_to_combine = combinable_files(data.base_filename(), data_paths)
|
||||
|
||||
if strict and not files_to_combine:
|
||||
raise NoDataError("No data to combine")
|
||||
raise NoDataError('No data to combine')
|
||||
|
||||
file_hashes = set()
|
||||
combined_any = False
|
||||
|
|
@ -141,8 +143,8 @@ def combine_parallel_data(
|
|||
if f == data.data_filename():
|
||||
# Sometimes we are combining into a file which is one of the
|
||||
# parallel files. Skip that file.
|
||||
if data._debug.should("dataio"):
|
||||
data._debug.write(f"Skipping combining ourself: {f!r}")
|
||||
if data._debug.should('dataio'):
|
||||
data._debug.write(f'Skipping combining ourself: {f!r}')
|
||||
continue
|
||||
|
||||
try:
|
||||
|
|
@ -153,16 +155,16 @@ def combine_parallel_data(
|
|||
# we print the original value of f instead of its relative path
|
||||
rel_file_name = f
|
||||
|
||||
with open(f, "rb") as fobj:
|
||||
hasher = hashlib.new("sha3_256")
|
||||
with open(f, 'rb') as fobj:
|
||||
hasher = hashlib.new('sha3_256')
|
||||
hasher.update(fobj.read())
|
||||
sha = hasher.digest()
|
||||
combine_this_one = sha not in file_hashes
|
||||
|
||||
delete_this_one = not keep
|
||||
if combine_this_one:
|
||||
if data._debug.should("dataio"):
|
||||
data._debug.write(f"Combining data file {f!r}")
|
||||
if data._debug.should('dataio'):
|
||||
data._debug.write(f'Combining data file {f!r}')
|
||||
file_hashes.add(sha)
|
||||
try:
|
||||
new_data = CoverageData(f, debug=data._debug)
|
||||
|
|
@ -179,39 +181,39 @@ def combine_parallel_data(
|
|||
data.update(new_data, aliases=aliases)
|
||||
combined_any = True
|
||||
if message:
|
||||
message(f"Combined data file {rel_file_name}")
|
||||
message(f'Combined data file {rel_file_name}')
|
||||
else:
|
||||
if message:
|
||||
message(f"Skipping duplicate data {rel_file_name}")
|
||||
message(f'Skipping duplicate data {rel_file_name}')
|
||||
|
||||
if delete_this_one:
|
||||
if data._debug.should("dataio"):
|
||||
data._debug.write(f"Deleting data file {f!r}")
|
||||
if data._debug.should('dataio'):
|
||||
data._debug.write(f'Deleting data file {f!r}')
|
||||
file_be_gone(f)
|
||||
|
||||
if strict and not combined_any:
|
||||
raise NoDataError("No usable data files")
|
||||
raise NoDataError('No usable data files')
|
||||
|
||||
|
||||
def debug_data_file(filename: str) -> None:
|
||||
"""Implementation of 'coverage debug data'."""
|
||||
data = CoverageData(filename)
|
||||
filename = data.data_filename()
|
||||
print(f"path: {filename}")
|
||||
print(f'path: {filename}')
|
||||
if not os.path.exists(filename):
|
||||
print("No data collected: file doesn't exist")
|
||||
return
|
||||
data.read()
|
||||
print(f"has_arcs: {data.has_arcs()!r}")
|
||||
print(f'has_arcs: {data.has_arcs()!r}')
|
||||
summary = line_counts(data, fullpath=True)
|
||||
filenames = human_sorted(summary.keys())
|
||||
nfiles = len(filenames)
|
||||
print(f"{nfiles} file{plural(nfiles)}:")
|
||||
print(f'{nfiles} file{plural(nfiles)}:')
|
||||
for f in filenames:
|
||||
line = f"{f}: {summary[f]} line{plural(summary[f])}"
|
||||
line = f'{f}: {summary[f]} line{plural(summary[f])}'
|
||||
plugin = data.file_tracer(f)
|
||||
if plugin:
|
||||
line += f" [{plugin}]"
|
||||
line += f' [{plugin}]'
|
||||
print(line)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Control of and utilities for debugging."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import _thread
|
||||
import atexit
|
||||
import contextlib
|
||||
import functools
|
||||
|
|
@ -17,15 +16,18 @@ import reprlib
|
|||
import sys
|
||||
import traceback
|
||||
import types
|
||||
import _thread
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import IO
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import Mapping
|
||||
from typing import overload
|
||||
|
||||
from typing import (
|
||||
overload,
|
||||
Any, Callable, IO, Iterable, Iterator, Mapping,
|
||||
)
|
||||
|
||||
from coverage.misc import human_sorted_items, isolate_module
|
||||
from coverage.types import AnyCallable, TWritable
|
||||
from coverage.misc import human_sorted_items
|
||||
from coverage.misc import isolate_module
|
||||
from coverage.types import AnyCallable
|
||||
from coverage.types import TWritable
|
||||
|
||||
os = isolate_module(os)
|
||||
|
||||
|
|
@ -53,12 +55,12 @@ class DebugControl:
|
|||
self.suppress_callers = False
|
||||
|
||||
filters = []
|
||||
if self.should("process"):
|
||||
if self.should('process'):
|
||||
filters.append(CwdTracker().filter)
|
||||
filters.append(ProcessTracker().filter)
|
||||
if self.should("pytest"):
|
||||
if self.should('pytest'):
|
||||
filters.append(PytestTracker().filter)
|
||||
if self.should("pid"):
|
||||
if self.should('pid'):
|
||||
filters.append(add_pid_and_tid)
|
||||
|
||||
self.output = DebugOutputFile.get_one(
|
||||
|
|
@ -69,11 +71,11 @@ class DebugControl:
|
|||
self.raw_output = self.output.outfile
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<DebugControl options={self.options!r} raw_output={self.raw_output!r}>"
|
||||
return f'<DebugControl options={self.options!r} raw_output={self.raw_output!r}>'
|
||||
|
||||
def should(self, option: str) -> bool:
|
||||
"""Decide whether to output debug information in category `option`."""
|
||||
if option == "callers" and self.suppress_callers:
|
||||
if option == 'callers' and self.suppress_callers:
|
||||
return False
|
||||
return (option in self.options)
|
||||
|
||||
|
|
@ -96,20 +98,21 @@ class DebugControl:
|
|||
after the message.
|
||||
|
||||
"""
|
||||
self.output.write(msg + "\n")
|
||||
self.output.write(msg + '\n')
|
||||
if exc is not None:
|
||||
self.output.write("".join(traceback.format_exception(None, exc, exc.__traceback__)))
|
||||
if self.should("self"):
|
||||
caller_self = inspect.stack()[1][0].f_locals.get("self")
|
||||
self.output.write(''.join(traceback.format_exception(None, exc, exc.__traceback__)))
|
||||
if self.should('self'):
|
||||
caller_self = inspect.stack()[1][0].f_locals.get('self')
|
||||
if caller_self is not None:
|
||||
self.output.write(f"self: {caller_self!r}\n")
|
||||
if self.should("callers"):
|
||||
self.output.write(f'self: {caller_self!r}\n')
|
||||
if self.should('callers'):
|
||||
dump_stack_frames(out=self.output, skip=1)
|
||||
self.output.flush()
|
||||
|
||||
|
||||
class NoDebugging(DebugControl):
|
||||
"""A replacement for DebugControl that will never try to do anything."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# pylint: disable=super-init-not-called
|
||||
...
|
||||
|
|
@ -120,12 +123,12 @@ class NoDebugging(DebugControl):
|
|||
|
||||
def write(self, msg: str, *, exc: BaseException | None = None) -> None:
|
||||
"""This will never be called."""
|
||||
raise AssertionError("NoDebugging.write should never be called.")
|
||||
raise AssertionError('NoDebugging.write should never be called.')
|
||||
|
||||
|
||||
def info_header(label: str) -> str:
|
||||
"""Make a nice header string."""
|
||||
return "--{:-<60s}".format(" "+label+" ")
|
||||
return '--{:-<60s}'.format(' ' + label + ' ')
|
||||
|
||||
|
||||
def info_formatter(info: Iterable[tuple[str, Any]]) -> Iterator[str]:
|
||||
|
|
@ -142,17 +145,17 @@ def info_formatter(info: Iterable[tuple[str, Any]]) -> Iterator[str]:
|
|||
assert all(len(l) < label_len for l, _ in info)
|
||||
for label, data in info:
|
||||
if data == []:
|
||||
data = "-none-"
|
||||
data = '-none-'
|
||||
if isinstance(data, tuple) and len(repr(tuple(data))) < 30:
|
||||
# Convert to tuple to scrub namedtuples.
|
||||
yield "%*s: %r" % (label_len, label, tuple(data))
|
||||
yield '%*s: %r' % (label_len, label, tuple(data))
|
||||
elif isinstance(data, (list, set, tuple)):
|
||||
prefix = "%*s:" % (label_len, label)
|
||||
prefix = '%*s:' % (label_len, label)
|
||||
for e in data:
|
||||
yield "%*s %s" % (label_len+1, prefix, e)
|
||||
prefix = ""
|
||||
yield '%*s %s' % (label_len + 1, prefix, e)
|
||||
prefix = ''
|
||||
else:
|
||||
yield "%*s: %s" % (label_len, label, data)
|
||||
yield '%*s: %s' % (label_len, label, data)
|
||||
|
||||
|
||||
def write_formatted_info(
|
||||
|
|
@ -170,35 +173,38 @@ def write_formatted_info(
|
|||
"""
|
||||
write(info_header(header))
|
||||
for line in info_formatter(info):
|
||||
write(f" {line}")
|
||||
write(f' {line}')
|
||||
|
||||
|
||||
def exc_one_line(exc: Exception) -> str:
|
||||
"""Get a one-line summary of an exception, including class name and message."""
|
||||
lines = traceback.format_exception_only(type(exc), exc)
|
||||
return "|".join(l.rstrip() for l in lines)
|
||||
return '|'.join(l.rstrip() for l in lines)
|
||||
|
||||
|
||||
_FILENAME_REGEXES: list[tuple[str, str]] = [
|
||||
(r".*[/\\]pytest-of-.*[/\\]pytest-\d+([/\\]popen-gw\d+)?", "tmp:"),
|
||||
(r'.*[/\\]pytest-of-.*[/\\]pytest-\d+([/\\]popen-gw\d+)?', 'tmp:'),
|
||||
]
|
||||
_FILENAME_SUBS: list[tuple[str, str]] = []
|
||||
|
||||
|
||||
@overload
|
||||
def short_filename(filename: str) -> str:
|
||||
pass
|
||||
|
||||
|
||||
@overload
|
||||
def short_filename(filename: None) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def short_filename(filename: str | None) -> str | None:
|
||||
"""Shorten a file name. Directories are replaced by prefixes like 'syspath:'"""
|
||||
if not _FILENAME_SUBS:
|
||||
for pathdir in sys.path:
|
||||
_FILENAME_SUBS.append((pathdir, "syspath:"))
|
||||
_FILENAME_SUBS.append((pathdir, 'syspath:'))
|
||||
import coverage
|
||||
_FILENAME_SUBS.append((os.path.dirname(coverage.__file__), "cov:"))
|
||||
_FILENAME_SUBS.append((os.path.dirname(coverage.__file__), 'cov:'))
|
||||
_FILENAME_SUBS.sort(key=(lambda pair: len(pair[0])), reverse=True)
|
||||
if filename is not None:
|
||||
for pat, sub in _FILENAME_REGEXES:
|
||||
|
|
@ -237,9 +243,9 @@ def short_stack(
|
|||
"""
|
||||
# Regexes in initial frames that we don't care about.
|
||||
BORING_PRELUDE = [
|
||||
"<string>", # pytest-xdist has string execution.
|
||||
r"\bigor.py$", # Our test runner.
|
||||
r"\bsite-packages\b", # pytest etc getting to our tests.
|
||||
'<string>', # pytest-xdist has string execution.
|
||||
r'\bigor.py$', # Our test runner.
|
||||
r'\bsite-packages\b', # pytest etc getting to our tests.
|
||||
]
|
||||
|
||||
stack: Iterable[inspect.FrameInfo] = inspect.stack()[:skip:-1]
|
||||
|
|
@ -251,20 +257,20 @@ def short_stack(
|
|||
)
|
||||
lines = []
|
||||
for frame_info in stack:
|
||||
line = f"{frame_info.function:>30s} : "
|
||||
line = f'{frame_info.function:>30s} : '
|
||||
if frame_ids:
|
||||
line += f"{id(frame_info.frame):#x} "
|
||||
line += f'{id(frame_info.frame):#x} '
|
||||
filename = frame_info.filename
|
||||
if short_filenames:
|
||||
filename = short_filename(filename)
|
||||
line += f"{filename}:{frame_info.lineno}"
|
||||
line += f'{filename}:{frame_info.lineno}'
|
||||
lines.append(line)
|
||||
return "\n".join(lines)
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def dump_stack_frames(out: TWritable, skip: int = 0) -> None:
|
||||
"""Print a summary of the stack to `out`."""
|
||||
out.write(short_stack(skip=skip+1) + "\n")
|
||||
out.write(short_stack(skip=skip + 1) + '\n')
|
||||
|
||||
|
||||
def clipped_repr(text: str, numchars: int = 50) -> str:
|
||||
|
|
@ -285,36 +291,37 @@ def short_id(id64: int) -> int:
|
|||
def add_pid_and_tid(text: str) -> str:
|
||||
"""A filter to add pid and tid to debug messages."""
|
||||
# Thread ids are useful, but too long. Make a shorter one.
|
||||
tid = f"{short_id(_thread.get_ident()):04x}"
|
||||
text = f"{os.getpid():5d}.{tid}: {text}"
|
||||
tid = f'{short_id(_thread.get_ident()):04x}'
|
||||
text = f'{os.getpid():5d}.{tid}: {text}'
|
||||
return text
|
||||
|
||||
|
||||
AUTO_REPR_IGNORE = {"$coverage.object_id"}
|
||||
AUTO_REPR_IGNORE = {'$coverage.object_id'}
|
||||
|
||||
|
||||
def auto_repr(self: Any) -> str:
|
||||
"""A function implementing an automatic __repr__ for debugging."""
|
||||
show_attrs = (
|
||||
(k, v) for k, v in self.__dict__.items()
|
||||
if getattr(v, "show_repr_attr", True)
|
||||
and not inspect.ismethod(v)
|
||||
and k not in AUTO_REPR_IGNORE
|
||||
if getattr(v, 'show_repr_attr', True) and
|
||||
not inspect.ismethod(v) and
|
||||
k not in AUTO_REPR_IGNORE
|
||||
)
|
||||
return "<{klass} @{id:#x}{attrs}>".format(
|
||||
return '<{klass} @{id:#x}{attrs}>'.format(
|
||||
klass=self.__class__.__name__,
|
||||
id=id(self),
|
||||
attrs="".join(f" {k}={v!r}" for k, v in show_attrs),
|
||||
attrs=''.join(f' {k}={v!r}' for k, v in show_attrs),
|
||||
)
|
||||
|
||||
|
||||
def simplify(v: Any) -> Any: # pragma: debugging
|
||||
"""Turn things which are nearly dict/list/etc into dict/list/etc."""
|
||||
if isinstance(v, dict):
|
||||
return {k:simplify(vv) for k, vv in v.items()}
|
||||
return {k: simplify(vv) for k, vv in v.items()}
|
||||
elif isinstance(v, (list, tuple)):
|
||||
return type(v)(simplify(vv) for vv in v)
|
||||
elif hasattr(v, "__dict__"):
|
||||
return simplify({"."+k: v for k, v in v.__dict__.items()})
|
||||
elif hasattr(v, '__dict__'):
|
||||
return simplify({'.' + k: v for k, v in v.__dict__.items()})
|
||||
else:
|
||||
return v
|
||||
|
||||
|
|
@ -343,12 +350,13 @@ def filter_text(text: str, filters: Iterable[Callable[[str], str]]) -> str:
|
|||
lines = []
|
||||
for line in text.splitlines():
|
||||
lines.extend(filter_fn(line).splitlines())
|
||||
text = "\n".join(lines)
|
||||
text = '\n'.join(lines)
|
||||
return text + ending
|
||||
|
||||
|
||||
class CwdTracker:
|
||||
"""A class to add cwd info to debug messages."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.cwd: str | None = None
|
||||
|
||||
|
|
@ -356,32 +364,33 @@ class CwdTracker:
|
|||
"""Add a cwd message for each new cwd."""
|
||||
cwd = os.getcwd()
|
||||
if cwd != self.cwd:
|
||||
text = f"cwd is now {cwd!r}\n" + text
|
||||
text = f'cwd is now {cwd!r}\n' + text
|
||||
self.cwd = cwd
|
||||
return text
|
||||
|
||||
|
||||
class ProcessTracker:
|
||||
"""Track process creation for debug logging."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.pid: int = os.getpid()
|
||||
self.did_welcome = False
|
||||
|
||||
def filter(self, text: str) -> str:
|
||||
"""Add a message about how new processes came to be."""
|
||||
welcome = ""
|
||||
welcome = ''
|
||||
pid = os.getpid()
|
||||
if self.pid != pid:
|
||||
welcome = f"New process: forked {self.pid} -> {pid}\n"
|
||||
welcome = f'New process: forked {self.pid} -> {pid}\n'
|
||||
self.pid = pid
|
||||
elif not self.did_welcome:
|
||||
argv = getattr(sys, "argv", None)
|
||||
argv = getattr(sys, 'argv', None)
|
||||
welcome = (
|
||||
f"New process: {pid=}, executable: {sys.executable!r}\n"
|
||||
+ f"New process: cmd: {argv!r}\n"
|
||||
f'New process: {pid=}, executable: {sys.executable!r}\n' +
|
||||
f'New process: cmd: {argv!r}\n'
|
||||
)
|
||||
if hasattr(os, "getppid"):
|
||||
welcome += f"New process parent pid: {os.getppid()!r}\n"
|
||||
if hasattr(os, 'getppid'):
|
||||
welcome += f'New process parent pid: {os.getppid()!r}\n'
|
||||
|
||||
if welcome:
|
||||
self.did_welcome = True
|
||||
|
|
@ -392,20 +401,22 @@ class ProcessTracker:
|
|||
|
||||
class PytestTracker:
|
||||
"""Track the current pytest test name to add to debug messages."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.test_name: str | None = None
|
||||
|
||||
def filter(self, text: str) -> str:
|
||||
"""Add a message when the pytest test changes."""
|
||||
test_name = os.getenv("PYTEST_CURRENT_TEST")
|
||||
test_name = os.getenv('PYTEST_CURRENT_TEST')
|
||||
if test_name != self.test_name:
|
||||
text = f"Pytest context: {test_name}\n" + text
|
||||
text = f'Pytest context: {test_name}\n' + text
|
||||
self.test_name = test_name
|
||||
return text
|
||||
|
||||
|
||||
class DebugOutputFile:
|
||||
"""A file-like object that includes pid and cwd information."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
outfile: IO[str] | None,
|
||||
|
|
@ -444,21 +455,21 @@ class DebugOutputFile:
|
|||
the_one, is_interim = cls._get_singleton_data()
|
||||
if the_one is None or is_interim:
|
||||
if file_name is not None:
|
||||
fileobj = open(file_name, "a", encoding="utf-8")
|
||||
fileobj = open(file_name, 'a', encoding='utf-8')
|
||||
else:
|
||||
# $set_env.py: COVERAGE_DEBUG_FILE - Where to write debug output
|
||||
file_name = os.getenv("COVERAGE_DEBUG_FILE", FORCED_DEBUG_FILE)
|
||||
if file_name in ("stdout", "stderr"):
|
||||
file_name = os.getenv('COVERAGE_DEBUG_FILE', FORCED_DEBUG_FILE)
|
||||
if file_name in ('stdout', 'stderr'):
|
||||
fileobj = getattr(sys, file_name)
|
||||
elif file_name:
|
||||
fileobj = open(file_name, "a", encoding="utf-8")
|
||||
fileobj = open(file_name, 'a', encoding='utf-8')
|
||||
atexit.register(fileobj.close)
|
||||
else:
|
||||
fileobj = sys.stderr
|
||||
the_one = cls(fileobj, filters)
|
||||
cls._set_singleton_data(the_one, interim)
|
||||
|
||||
if not(the_one.filters):
|
||||
if not (the_one.filters):
|
||||
the_one.filters = list(filters)
|
||||
return the_one
|
||||
|
||||
|
|
@ -467,8 +478,8 @@ class DebugOutputFile:
|
|||
# a process-wide singleton. So stash it in sys.modules instead of
|
||||
# on a class attribute. Yes, this is aggressively gross.
|
||||
|
||||
SYS_MOD_NAME = "$coverage.debug.DebugOutputFile.the_one"
|
||||
SINGLETON_ATTR = "the_one_and_is_interim"
|
||||
SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one'
|
||||
SINGLETON_ATTR = 'the_one_and_is_interim'
|
||||
|
||||
@classmethod
|
||||
def _set_singleton_data(cls, the_one: DebugOutputFile, interim: bool) -> None:
|
||||
|
|
@ -504,7 +515,7 @@ class DebugOutputFile:
|
|||
def log(msg: str, stack: bool = False) -> None: # pragma: debugging
|
||||
"""Write a log message as forcefully as possible."""
|
||||
out = DebugOutputFile.get_one(interim=True)
|
||||
out.write(msg+"\n")
|
||||
out.write(msg + '\n')
|
||||
if stack:
|
||||
dump_stack_frames(out=out, skip=1)
|
||||
|
||||
|
|
@ -519,8 +530,8 @@ def decorate_methods(
|
|||
for name, meth in inspect.getmembers(cls, inspect.isroutine):
|
||||
if name not in cls.__dict__:
|
||||
continue
|
||||
if name != "__init__":
|
||||
if not private and name.startswith("_"):
|
||||
if name != '__init__':
|
||||
if not private and name.startswith('_'):
|
||||
continue
|
||||
if name in butnot:
|
||||
continue
|
||||
|
|
@ -542,7 +553,8 @@ def break_in_pudb(func: AnyCallable) -> AnyCallable: # pragma: debugging
|
|||
|
||||
OBJ_IDS = itertools.count()
|
||||
CALLS = itertools.count()
|
||||
OBJ_ID_ATTR = "$coverage.object_id"
|
||||
OBJ_ID_ATTR = '$coverage.object_id'
|
||||
|
||||
|
||||
def show_calls(
|
||||
show_args: bool = True,
|
||||
|
|
@ -555,27 +567,27 @@ def show_calls(
|
|||
def _wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
||||
oid = getattr(self, OBJ_ID_ATTR, None)
|
||||
if oid is None:
|
||||
oid = f"{os.getpid():08d} {next(OBJ_IDS):04d}"
|
||||
oid = f'{os.getpid():08d} {next(OBJ_IDS):04d}'
|
||||
setattr(self, OBJ_ID_ATTR, oid)
|
||||
extra = ""
|
||||
extra = ''
|
||||
if show_args:
|
||||
eargs = ", ".join(map(repr, args))
|
||||
ekwargs = ", ".join("{}={!r}".format(*item) for item in kwargs.items())
|
||||
extra += "("
|
||||
eargs = ', '.join(map(repr, args))
|
||||
ekwargs = ', '.join('{}={!r}'.format(*item) for item in kwargs.items())
|
||||
extra += '('
|
||||
extra += eargs
|
||||
if eargs and ekwargs:
|
||||
extra += ", "
|
||||
extra += ', '
|
||||
extra += ekwargs
|
||||
extra += ")"
|
||||
extra += ')'
|
||||
if show_stack:
|
||||
extra += " @ "
|
||||
extra += "; ".join(short_stack(short_filenames=True).splitlines())
|
||||
extra += ' @ '
|
||||
extra += '; '.join(short_stack(short_filenames=True).splitlines())
|
||||
callid = next(CALLS)
|
||||
msg = f"{oid} {callid:04d} {func.__name__}{extra}\n"
|
||||
msg = f'{oid} {callid:04d} {func.__name__}{extra}\n'
|
||||
DebugOutputFile.get_one(interim=True).write(msg)
|
||||
ret = func(self, *args, **kwargs)
|
||||
if show_return:
|
||||
msg = f"{oid} {callid:04d} {func.__name__} return {ret!r}\n"
|
||||
msg = f'{oid} {callid:04d} {func.__name__} return {ret!r}\n'
|
||||
DebugOutputFile.get_one(interim=True).write(msg)
|
||||
return ret
|
||||
return _wrapper
|
||||
|
|
@ -595,9 +607,9 @@ def relevant_environment_display(env: Mapping[str, str]) -> list[tuple[str, str]
|
|||
A list of pairs (name, value) to show.
|
||||
|
||||
"""
|
||||
slugs = {"COV", "PY"}
|
||||
include = {"HOME", "TEMP", "TMP"}
|
||||
cloak = {"API", "TOKEN", "KEY", "SECRET", "PASS", "SIGNATURE"}
|
||||
slugs = {'COV', 'PY'}
|
||||
include = {'HOME', 'TEMP', 'TMP'}
|
||||
cloak = {'API', 'TOKEN', 'KEY', 'SECRET', 'PASS', 'SIGNATURE'}
|
||||
|
||||
to_show = []
|
||||
for name, val in env.items():
|
||||
|
|
@ -608,6 +620,6 @@ def relevant_environment_display(env: Mapping[str, str]) -> list[tuple[str, str]
|
|||
keep = True
|
||||
if keep:
|
||||
if any(slug in name for slug in cloak):
|
||||
val = re.sub(r"\w", "*", val)
|
||||
val = re.sub(r'\w', '*', val)
|
||||
to_show.append((name, val))
|
||||
return human_sorted_items(to_show)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Simple value objects for tracking what to do with files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
|
@ -25,7 +23,7 @@ class FileDisposition:
|
|||
has_dynamic_filename: bool
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<FileDisposition {self.canonical_filename!r}: trace={self.trace}>"
|
||||
return f'<FileDisposition {self.canonical_filename!r}: trace={self.trace}>'
|
||||
|
||||
|
||||
# FileDisposition "methods": FileDisposition is a pure value object, so it can
|
||||
|
|
@ -39,7 +37,7 @@ def disposition_init(cls: type[TFileDisposition], original_filename: str) -> TFi
|
|||
disp.canonical_filename = original_filename
|
||||
disp.source_filename = None
|
||||
disp.trace = False
|
||||
disp.reason = ""
|
||||
disp.reason = ''
|
||||
disp.file_tracer = None
|
||||
disp.has_dynamic_filename = False
|
||||
return disp
|
||||
|
|
@ -48,11 +46,11 @@ def disposition_init(cls: type[TFileDisposition], original_filename: str) -> TFi
|
|||
def disposition_debug_msg(disp: TFileDisposition) -> str:
|
||||
"""Make a nice debug message of what the FileDisposition is doing."""
|
||||
if disp.trace:
|
||||
msg = f"Tracing {disp.original_filename!r}"
|
||||
msg = f'Tracing {disp.original_filename!r}'
|
||||
if disp.original_filename != disp.source_filename:
|
||||
msg += f" as {disp.source_filename!r}"
|
||||
msg += f' as {disp.source_filename!r}'
|
||||
if disp.file_tracer:
|
||||
msg += f": will be traced by {disp.file_tracer!r}"
|
||||
msg += f': will be traced by {disp.file_tracer!r}'
|
||||
else:
|
||||
msg = f"Not tracing {disp.original_filename!r}: {disp.reason}"
|
||||
msg = f'Not tracing {disp.original_filename!r}: {disp.reason}'
|
||||
return msg
|
||||
|
|
|
|||
|
|
@ -1,48 +1,48 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Determine facts about the environment."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
|
||||
from typing import Any, Iterable
|
||||
from typing import Any
|
||||
from typing import Iterable
|
||||
|
||||
# debug_info() at the bottom wants to show all the globals, but not imports.
|
||||
# Grab the global names here to know which names to not show. Nothing defined
|
||||
# above this line will be in the output.
|
||||
_UNINTERESTING_GLOBALS = list(globals())
|
||||
# These names also shouldn't be shown.
|
||||
_UNINTERESTING_GLOBALS += ["PYBEHAVIOR", "debug_info"]
|
||||
_UNINTERESTING_GLOBALS += ['PYBEHAVIOR', 'debug_info']
|
||||
|
||||
# Operating systems.
|
||||
WINDOWS = sys.platform == "win32"
|
||||
LINUX = sys.platform.startswith("linux")
|
||||
OSX = sys.platform == "darwin"
|
||||
WINDOWS = sys.platform == 'win32'
|
||||
LINUX = sys.platform.startswith('linux')
|
||||
OSX = sys.platform == 'darwin'
|
||||
|
||||
# Python implementations.
|
||||
CPYTHON = (platform.python_implementation() == "CPython")
|
||||
PYPY = (platform.python_implementation() == "PyPy")
|
||||
CPYTHON = (platform.python_implementation() == 'CPython')
|
||||
PYPY = (platform.python_implementation() == 'PyPy')
|
||||
|
||||
# Python versions. We amend version_info with one more value, a zero if an
|
||||
# official version, or 1 if built from source beyond an official version.
|
||||
# Only use sys.version_info directly where tools like mypy need it to understand
|
||||
# version-specfic code, otherwise use PYVERSION.
|
||||
PYVERSION = sys.version_info + (int(platform.python_version()[-1] == "+"),)
|
||||
PYVERSION = sys.version_info + (int(platform.python_version()[-1] == '+'),)
|
||||
|
||||
if PYPY:
|
||||
PYPYVERSION = sys.pypy_version_info # type: ignore[attr-defined]
|
||||
|
||||
# Python behavior.
|
||||
|
||||
|
||||
class PYBEHAVIOR:
|
||||
"""Flags indicating this Python's behavior."""
|
||||
|
||||
# Does Python conform to PEP626, Precise line numbers for debugging and other tools.
|
||||
# https://www.python.org/dev/peps/pep-0626
|
||||
pep626 = (PYVERSION > (3, 10, 0, "alpha", 4))
|
||||
pep626 = (PYVERSION > (3, 10, 0, 'alpha', 4))
|
||||
|
||||
# Is "if __debug__" optimized away?
|
||||
optimize_if_debug = not pep626
|
||||
|
|
@ -69,19 +69,19 @@ class PYBEHAVIOR:
|
|||
|
||||
# CPython 3.11 now jumps to the decorator line again while executing
|
||||
# the decorator.
|
||||
trace_decorator_line_again = (CPYTHON and PYVERSION > (3, 11, 0, "alpha", 3, 0))
|
||||
trace_decorator_line_again = (CPYTHON and PYVERSION > (3, 11, 0, 'alpha', 3, 0))
|
||||
|
||||
# CPython 3.9a1 made sys.argv[0] and other reported files absolute paths.
|
||||
report_absolute_files = (
|
||||
(CPYTHON or (PYPY and PYPYVERSION >= (7, 3, 10)))
|
||||
and PYVERSION >= (3, 9)
|
||||
(CPYTHON or (PYPY and PYPYVERSION >= (7, 3, 10))) and
|
||||
PYVERSION >= (3, 9)
|
||||
)
|
||||
|
||||
# Lines after break/continue/return/raise are no longer compiled into the
|
||||
# bytecode. They used to be marked as missing, now they aren't executable.
|
||||
omit_after_jump = (
|
||||
pep626
|
||||
or (PYPY and PYVERSION >= (3, 9) and PYPYVERSION >= (7, 3, 12))
|
||||
pep626 or
|
||||
(PYPY and PYVERSION >= (3, 9) and PYPYVERSION >= (7, 3, 12))
|
||||
)
|
||||
|
||||
# PyPy has always omitted statements after return.
|
||||
|
|
@ -98,7 +98,7 @@ class PYBEHAVIOR:
|
|||
keep_constant_test = pep626
|
||||
|
||||
# When leaving a with-block, do we visit the with-line again for the exit?
|
||||
exit_through_with = (PYVERSION >= (3, 10, 0, "beta"))
|
||||
exit_through_with = (PYVERSION >= (3, 10, 0, 'beta'))
|
||||
|
||||
# Match-case construct.
|
||||
match_case = (PYVERSION >= (3, 10))
|
||||
|
|
@ -108,14 +108,14 @@ class PYBEHAVIOR:
|
|||
|
||||
# Modules start with a line numbered zero. This means empty modules have
|
||||
# only a 0-number line, which is ignored, giving a truly empty module.
|
||||
empty_is_empty = (PYVERSION >= (3, 11, 0, "beta", 4))
|
||||
empty_is_empty = (PYVERSION >= (3, 11, 0, 'beta', 4))
|
||||
|
||||
# Are comprehensions inlined (new) or compiled as called functions (old)?
|
||||
# Changed in https://github.com/python/cpython/pull/101441
|
||||
comprehensions_are_functions = (PYVERSION <= (3, 12, 0, "alpha", 7, 0))
|
||||
comprehensions_are_functions = (PYVERSION <= (3, 12, 0, 'alpha', 7, 0))
|
||||
|
||||
# PEP669 Low Impact Monitoring: https://peps.python.org/pep-0669/
|
||||
pep669 = bool(getattr(sys, "monitoring", None))
|
||||
pep669 = bool(getattr(sys, 'monitoring', None))
|
||||
|
||||
# Where does frame.f_lasti point when yielding from a generator?
|
||||
# It used to point at the YIELD, now it points at the RESUME.
|
||||
|
|
@ -126,22 +126,22 @@ class PYBEHAVIOR:
|
|||
# Coverage.py specifics, about testing scenarios. See tests/testenv.py also.
|
||||
|
||||
# Are we coverage-measuring ourselves?
|
||||
METACOV = os.getenv("COVERAGE_COVERAGE") is not None
|
||||
METACOV = os.getenv('COVERAGE_COVERAGE') is not None
|
||||
|
||||
# Are we running our test suite?
|
||||
# Even when running tests, you can use COVERAGE_TESTING=0 to disable the
|
||||
# test-specific behavior like AST checking.
|
||||
TESTING = os.getenv("COVERAGE_TESTING") == "True"
|
||||
TESTING = os.getenv('COVERAGE_TESTING') == 'True'
|
||||
|
||||
|
||||
def debug_info() -> Iterable[tuple[str, Any]]:
|
||||
"""Return a list of (name, value) pairs for printing debug information."""
|
||||
info = [
|
||||
(name, value) for name, value in globals().items()
|
||||
if not name.startswith("_") and name not in _UNINTERESTING_GLOBALS
|
||||
if not name.startswith('_') and name not in _UNINTERESTING_GLOBALS
|
||||
]
|
||||
info += [
|
||||
(name, value) for name, value in PYBEHAVIOR.__dict__.items()
|
||||
if not name.startswith("_")
|
||||
if not name.startswith('_')
|
||||
]
|
||||
return sorted(info)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Exceptions coverage.py can raise."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class _BaseCoverageException(Exception):
|
||||
"""The base-base of all Coverage exceptions."""
|
||||
pass
|
||||
|
|
@ -24,6 +23,7 @@ class DataError(CoverageException):
|
|||
"""An error in using a data file."""
|
||||
pass
|
||||
|
||||
|
||||
class NoDataError(CoverageException):
|
||||
"""We didn't have data to work with."""
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Execute files of Python code."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.machinery
|
||||
|
|
@ -12,14 +10,18 @@ import marshal
|
|||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from importlib.machinery import ModuleSpec
|
||||
from types import CodeType, ModuleType
|
||||
from types import CodeType
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
from coverage import env
|
||||
from coverage.exceptions import CoverageException, _ExceptionDuringRun, NoCode, NoSource
|
||||
from coverage.files import canonical_filename, python_reported_file
|
||||
from coverage.exceptions import _ExceptionDuringRun
|
||||
from coverage.exceptions import CoverageException
|
||||
from coverage.exceptions import NoCode
|
||||
from coverage.exceptions import NoSource
|
||||
from coverage.files import canonical_filename
|
||||
from coverage.files import python_reported_file
|
||||
from coverage.misc import isolate_module
|
||||
from coverage.python import get_python_source
|
||||
|
||||
|
|
@ -28,11 +30,13 @@ os = isolate_module(os)
|
|||
|
||||
PYC_MAGIC_NUMBER = importlib.util.MAGIC_NUMBER
|
||||
|
||||
|
||||
class DummyLoader:
|
||||
"""A shim for the pep302 __loader__, emulating pkgutil.ImpLoader.
|
||||
|
||||
Currently only implements the .fullname attribute
|
||||
"""
|
||||
|
||||
def __init__(self, fullname: str, *_args: Any) -> None:
|
||||
self.fullname = fullname
|
||||
|
||||
|
|
@ -50,20 +54,20 @@ def find_module(
|
|||
except ImportError as err:
|
||||
raise NoSource(str(err)) from err
|
||||
if not spec:
|
||||
raise NoSource(f"No module named {modulename!r}")
|
||||
raise NoSource(f'No module named {modulename!r}')
|
||||
pathname = spec.origin
|
||||
packagename = spec.name
|
||||
if spec.submodule_search_locations:
|
||||
mod_main = modulename + ".__main__"
|
||||
mod_main = modulename + '.__main__'
|
||||
spec = importlib.util.find_spec(mod_main)
|
||||
if not spec:
|
||||
raise NoSource(
|
||||
f"No module named {mod_main}; " +
|
||||
f"{modulename!r} is a package and cannot be directly executed",
|
||||
f'No module named {mod_main}; ' +
|
||||
f'{modulename!r} is a package and cannot be directly executed',
|
||||
)
|
||||
pathname = spec.origin
|
||||
packagename = spec.name
|
||||
packagename = packagename.rpartition(".")[0]
|
||||
packagename = packagename.rpartition('.')[0]
|
||||
return pathname, packagename, spec
|
||||
|
||||
|
||||
|
|
@ -73,6 +77,7 @@ class PyRunner:
|
|||
This is meant to emulate real Python execution as closely as possible.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, args: list[str], as_module: bool = False) -> None:
|
||||
self.args = args
|
||||
self.as_module = as_module
|
||||
|
|
@ -142,8 +147,8 @@ class PyRunner:
|
|||
elif os.path.isdir(self.arg0):
|
||||
# Running a directory means running the __main__.py file in that
|
||||
# directory.
|
||||
for ext in [".py", ".pyc", ".pyo"]:
|
||||
try_filename = os.path.join(self.arg0, "__main__" + ext)
|
||||
for ext in ['.py', '.pyc', '.pyo']:
|
||||
try_filename = os.path.join(self.arg0, '__main__' + ext)
|
||||
# 3.8.10 changed how files are reported when running a
|
||||
# directory. But I'm not sure how far this change is going to
|
||||
# spread, so I'll just hard-code it here for now.
|
||||
|
|
@ -157,12 +162,12 @@ class PyRunner:
|
|||
|
||||
# Make a spec. I don't know if this is the right way to do it.
|
||||
try_filename = python_reported_file(try_filename)
|
||||
self.spec = importlib.machinery.ModuleSpec("__main__", None, origin=try_filename)
|
||||
self.spec = importlib.machinery.ModuleSpec('__main__', None, origin=try_filename)
|
||||
self.spec.has_location = True
|
||||
self.package = ""
|
||||
self.loader = DummyLoader("__main__")
|
||||
self.package = ''
|
||||
self.loader = DummyLoader('__main__')
|
||||
else:
|
||||
self.loader = DummyLoader("__main__")
|
||||
self.loader = DummyLoader('__main__')
|
||||
|
||||
self.arg0 = python_reported_file(self.arg0)
|
||||
|
||||
|
|
@ -172,9 +177,9 @@ class PyRunner:
|
|||
self._prepare2()
|
||||
|
||||
# Create a module to serve as __main__
|
||||
main_mod = ModuleType("__main__")
|
||||
main_mod = ModuleType('__main__')
|
||||
|
||||
from_pyc = self.arg0.endswith((".pyc", ".pyo"))
|
||||
from_pyc = self.arg0.endswith(('.pyc', '.pyo'))
|
||||
main_mod.__file__ = self.arg0
|
||||
if from_pyc:
|
||||
main_mod.__file__ = main_mod.__file__[:-1]
|
||||
|
|
@ -184,9 +189,9 @@ class PyRunner:
|
|||
if self.spec is not None:
|
||||
main_mod.__spec__ = self.spec
|
||||
|
||||
main_mod.__builtins__ = sys.modules["builtins"] # type: ignore[attr-defined]
|
||||
main_mod.__builtins__ = sys.modules['builtins'] # type: ignore[attr-defined]
|
||||
|
||||
sys.modules["__main__"] = main_mod
|
||||
sys.modules['__main__'] = main_mod
|
||||
|
||||
# Set sys.argv properly.
|
||||
sys.argv = self.args
|
||||
|
|
@ -228,7 +233,7 @@ class PyRunner:
|
|||
# is non-None when the exception is reported at the upper layer,
|
||||
# and a nested exception is shown to the user. This getattr fixes
|
||||
# it somehow? https://bitbucket.org/pypy/pypy/issue/1903
|
||||
getattr(err, "__context__", None)
|
||||
getattr(err, '__context__', None)
|
||||
|
||||
# Call the excepthook.
|
||||
try:
|
||||
|
|
@ -240,7 +245,7 @@ class PyRunner:
|
|||
except Exception as exc:
|
||||
# Getting the output right in the case of excepthook
|
||||
# shenanigans is kind of involved.
|
||||
sys.stderr.write("Error in sys.excepthook:\n")
|
||||
sys.stderr.write('Error in sys.excepthook:\n')
|
||||
typ2, err2, tb2 = sys.exc_info()
|
||||
assert typ2 is not None
|
||||
assert err2 is not None
|
||||
|
|
@ -249,7 +254,7 @@ class PyRunner:
|
|||
assert err2.__traceback__ is not None
|
||||
err2.__traceback__ = err2.__traceback__.tb_next
|
||||
sys.__excepthook__(typ2, err2, tb2.tb_next)
|
||||
sys.stderr.write("\nOriginal exception was:\n")
|
||||
sys.stderr.write('\nOriginal exception was:\n')
|
||||
raise _ExceptionDuringRun(typ, err, tb.tb_next) from exc
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
|
@ -294,13 +299,13 @@ def make_code_from_py(filename: str) -> CodeType:
|
|||
except (OSError, NoSource) as exc:
|
||||
raise NoSource(f"No file to run: '{filename}'") from exc
|
||||
|
||||
return compile(source, filename, "exec", dont_inherit=True)
|
||||
return compile(source, filename, 'exec', dont_inherit=True)
|
||||
|
||||
|
||||
def make_code_from_pyc(filename: str) -> CodeType:
|
||||
"""Get a code object from a .pyc file."""
|
||||
try:
|
||||
fpyc = open(filename, "rb")
|
||||
fpyc = open(filename, 'rb')
|
||||
except OSError as exc:
|
||||
raise NoCode(f"No file to run: '{filename}'") from exc
|
||||
|
||||
|
|
@ -309,9 +314,9 @@ def make_code_from_pyc(filename: str) -> CodeType:
|
|||
# match or we won't run the file.
|
||||
magic = fpyc.read(4)
|
||||
if magic != PYC_MAGIC_NUMBER:
|
||||
raise NoCode(f"Bad magic number in .pyc file: {magic!r} != {PYC_MAGIC_NUMBER!r}")
|
||||
raise NoCode(f'Bad magic number in .pyc file: {magic!r} != {PYC_MAGIC_NUMBER!r}')
|
||||
|
||||
flags = struct.unpack("<L", fpyc.read(4))[0]
|
||||
flags = struct.unpack('<L', fpyc.read(4))[0]
|
||||
hash_based = flags & 0x01
|
||||
if hash_based:
|
||||
fpyc.read(8) # Skip the hash.
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""File wrangling."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import ntpath
|
||||
import os
|
||||
import os.path
|
||||
import posixpath
|
||||
import re
|
||||
import sys
|
||||
|
||||
from typing import Callable, Iterable
|
||||
from typing import Callable
|
||||
from typing import Iterable
|
||||
|
||||
from coverage import env
|
||||
from coverage.exceptions import ConfigError
|
||||
from coverage.misc import human_sorted, isolate_module, join_regex
|
||||
from coverage.misc import human_sorted
|
||||
from coverage.misc import isolate_module
|
||||
from coverage.misc import join_regex
|
||||
|
||||
|
||||
os = isolate_module(os)
|
||||
|
||||
|
||||
RELATIVE_DIR: str = ""
|
||||
RELATIVE_DIR: str = ''
|
||||
CANONICAL_FILENAME_CACHE: dict[str, str] = {}
|
||||
|
||||
|
||||
def set_relative_directory() -> None:
|
||||
"""Set the directory that `relative_filename` will be relative to."""
|
||||
global RELATIVE_DIR, CANONICAL_FILENAME_CACHE
|
||||
|
|
@ -73,7 +73,7 @@ def canonical_filename(filename: str) -> str:
|
|||
if not os.path.isabs(filename):
|
||||
for path in [os.curdir] + sys.path:
|
||||
if path is None:
|
||||
continue # type: ignore[unreachable]
|
||||
continue # type: ignore[unreachable]
|
||||
f = os.path.join(path, filename)
|
||||
try:
|
||||
exists = os.path.exists(f)
|
||||
|
|
@ -89,6 +89,7 @@ def canonical_filename(filename: str) -> str:
|
|||
|
||||
MAX_FLAT = 100
|
||||
|
||||
|
||||
def flat_rootname(filename: str) -> str:
|
||||
"""A base for a flat file name to correspond to this file.
|
||||
|
||||
|
|
@ -101,11 +102,11 @@ def flat_rootname(filename: str) -> str:
|
|||
"""
|
||||
dirname, basename = ntpath.split(filename)
|
||||
if dirname:
|
||||
fp = hashlib.new("sha3_256", dirname.encode("UTF-8")).hexdigest()[:16]
|
||||
prefix = f"d_{fp}_"
|
||||
fp = hashlib.new('sha3_256', dirname.encode('UTF-8')).hexdigest()[:16]
|
||||
prefix = f'd_{fp}_'
|
||||
else:
|
||||
prefix = ""
|
||||
return prefix + basename.replace(".", "_")
|
||||
prefix = ''
|
||||
return prefix + basename.replace('.', '_')
|
||||
|
||||
|
||||
if env.WINDOWS:
|
||||
|
|
@ -163,7 +164,7 @@ def zip_location(filename: str) -> tuple[str, str] | None:
|
|||
name is in the zipfile.
|
||||
|
||||
"""
|
||||
for ext in [".zip", ".whl", ".egg", ".pex"]:
|
||||
for ext in ['.zip', '.whl', '.egg', '.pex']:
|
||||
zipbase, extension, inner = filename.partition(ext + sep(filename))
|
||||
if extension:
|
||||
zipfile = zipbase + ext
|
||||
|
|
@ -210,7 +211,7 @@ def prep_patterns(patterns: Iterable[str]) -> list[str]:
|
|||
prepped = []
|
||||
for p in patterns or []:
|
||||
prepped.append(p)
|
||||
if not p.startswith(("*", "?")):
|
||||
if not p.startswith(('*', '?')):
|
||||
prepped.append(abs_file(p))
|
||||
return prepped
|
||||
|
||||
|
|
@ -223,14 +224,15 @@ class TreeMatcher:
|
|||
somewhere in a subtree rooted at one of the directories.
|
||||
|
||||
"""
|
||||
def __init__(self, paths: Iterable[str], name: str = "unknown") -> None:
|
||||
|
||||
def __init__(self, paths: Iterable[str], name: str = 'unknown') -> None:
|
||||
self.original_paths: list[str] = human_sorted(paths)
|
||||
#self.paths = list(map(os.path.normcase, paths))
|
||||
self.paths = [os.path.normcase(p) for p in paths]
|
||||
self.name = name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<TreeMatcher {self.name} {self.original_paths!r}>"
|
||||
return f'<TreeMatcher {self.name} {self.original_paths!r}>'
|
||||
|
||||
def info(self) -> list[str]:
|
||||
"""A list of strings for displaying when dumping state."""
|
||||
|
|
@ -252,12 +254,13 @@ class TreeMatcher:
|
|||
|
||||
class ModuleMatcher:
|
||||
"""A matcher for modules in a tree."""
|
||||
def __init__(self, module_names: Iterable[str], name:str = "unknown") -> None:
|
||||
|
||||
def __init__(self, module_names: Iterable[str], name: str = 'unknown') -> None:
|
||||
self.modules = list(module_names)
|
||||
self.name = name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<ModuleMatcher {self.name} {self.modules!r}>"
|
||||
return f'<ModuleMatcher {self.name} {self.modules!r}>'
|
||||
|
||||
def info(self) -> list[str]:
|
||||
"""A list of strings for displaying when dumping state."""
|
||||
|
|
@ -272,7 +275,7 @@ class ModuleMatcher:
|
|||
if module_name.startswith(m):
|
||||
if module_name == m:
|
||||
return True
|
||||
if module_name[len(m)] == ".":
|
||||
if module_name[len(m)] == '.':
|
||||
# This is a module in the package
|
||||
return True
|
||||
|
||||
|
|
@ -281,13 +284,14 @@ class ModuleMatcher:
|
|||
|
||||
class GlobMatcher:
|
||||
"""A matcher for files by file name pattern."""
|
||||
def __init__(self, pats: Iterable[str], name: str = "unknown") -> None:
|
||||
|
||||
def __init__(self, pats: Iterable[str], name: str = 'unknown') -> None:
|
||||
self.pats = list(pats)
|
||||
self.re = globs_to_regex(self.pats, case_insensitive=env.WINDOWS)
|
||||
self.name = name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<GlobMatcher {self.name} {self.pats!r}>"
|
||||
return f'<GlobMatcher {self.name} {self.pats!r}>'
|
||||
|
||||
def info(self) -> list[str]:
|
||||
"""A list of strings for displaying when dumping state."""
|
||||
|
|
@ -300,7 +304,7 @@ class GlobMatcher:
|
|||
|
||||
def sep(s: str) -> str:
|
||||
"""Find the path separator used in this string, or os.sep if none."""
|
||||
if sep_match := re.search(r"[\\/]", s):
|
||||
if sep_match := re.search(r'[\\/]', s):
|
||||
the_sep = sep_match[0]
|
||||
else:
|
||||
the_sep = os.sep
|
||||
|
|
@ -309,29 +313,32 @@ def sep(s: str) -> str:
|
|||
|
||||
# Tokenizer for _glob_to_regex.
|
||||
# None as a sub means disallowed.
|
||||
G2RX_TOKENS = [(re.compile(rx), sub) for rx, sub in [
|
||||
(r"\*\*\*+", None), # Can't have ***
|
||||
(r"[^/]+\*\*+", None), # Can't have x**
|
||||
(r"\*\*+[^/]+", None), # Can't have **x
|
||||
(r"\*\*/\*\*", None), # Can't have **/**
|
||||
(r"^\*+/", r"(.*[/\\\\])?"), # ^*/ matches any prefix-slash, or nothing.
|
||||
(r"/\*+$", r"[/\\\\].*"), # /*$ matches any slash-suffix.
|
||||
(r"\*\*/", r"(.*[/\\\\])?"), # **/ matches any subdirs, including none
|
||||
(r"/", r"[/\\\\]"), # / matches either slash or backslash
|
||||
(r"\*", r"[^/\\\\]*"), # * matches any number of non slash-likes
|
||||
(r"\?", r"[^/\\\\]"), # ? matches one non slash-like
|
||||
(r"\[.*?\]", r"\g<0>"), # [a-f] matches [a-f]
|
||||
(r"[a-zA-Z0-9_-]+", r"\g<0>"), # word chars match themselves
|
||||
(r"[\[\]]", None), # Can't have single square brackets
|
||||
(r".", r"\\\g<0>"), # Anything else is escaped to be safe
|
||||
]]
|
||||
G2RX_TOKENS = [
|
||||
(re.compile(rx), sub) for rx, sub in [
|
||||
(r'\*\*\*+', None), # Can't have ***
|
||||
(r'[^/]+\*\*+', None), # Can't have x**
|
||||
(r'\*\*+[^/]+', None), # Can't have **x
|
||||
(r'\*\*/\*\*', None), # Can't have **/**
|
||||
(r'^\*+/', r'(.*[/\\\\])?'), # ^*/ matches any prefix-slash, or nothing.
|
||||
(r'/\*+$', r'[/\\\\].*'), # /*$ matches any slash-suffix.
|
||||
(r'\*\*/', r'(.*[/\\\\])?'), # **/ matches any subdirs, including none
|
||||
(r'/', r'[/\\\\]'), # / matches either slash or backslash
|
||||
(r'\*', r'[^/\\\\]*'), # * matches any number of non slash-likes
|
||||
(r'\?', r'[^/\\\\]'), # ? matches one non slash-like
|
||||
(r'\[.*?\]', r'\g<0>'), # [a-f] matches [a-f]
|
||||
(r'[a-zA-Z0-9_-]+', r'\g<0>'), # word chars match themselves
|
||||
(r'[\[\]]', None), # Can't have single square brackets
|
||||
(r'.', r'\\\g<0>'), # Anything else is escaped to be safe
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
def _glob_to_regex(pattern: str) -> str:
|
||||
"""Convert a file-path glob pattern into a regex."""
|
||||
# Turn all backslashes into slashes to simplify the tokenizer.
|
||||
pattern = pattern.replace("\\", "/")
|
||||
if "/" not in pattern:
|
||||
pattern = "**/" + pattern
|
||||
pattern = pattern.replace('\\', '/')
|
||||
if '/' not in pattern:
|
||||
pattern = '**/' + pattern
|
||||
path_rx = []
|
||||
pos = 0
|
||||
while pos < len(pattern):
|
||||
|
|
@ -342,7 +349,7 @@ def _glob_to_regex(pattern: str) -> str:
|
|||
path_rx.append(m.expand(sub))
|
||||
pos = m.end()
|
||||
break
|
||||
return "".join(path_rx)
|
||||
return ''.join(path_rx)
|
||||
|
||||
|
||||
def globs_to_regex(
|
||||
|
|
@ -371,7 +378,7 @@ def globs_to_regex(
|
|||
flags |= re.IGNORECASE
|
||||
rx = join_regex(map(_glob_to_regex, patterns))
|
||||
if not partial:
|
||||
rx = fr"(?:{rx})\Z"
|
||||
rx = fr'(?:{rx})\Z'
|
||||
compiled = re.compile(rx, flags=flags)
|
||||
return compiled
|
||||
|
||||
|
|
@ -387,6 +394,7 @@ class PathAliases:
|
|||
map a path through those aliases to produce a unified path.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
debugfn: Callable[[str], None] | None = None,
|
||||
|
|
@ -400,9 +408,9 @@ class PathAliases:
|
|||
|
||||
def pprint(self) -> None:
|
||||
"""Dump the important parts of the PathAliases, for debugging."""
|
||||
self.debugfn(f"Aliases (relative={self.relative}):")
|
||||
self.debugfn(f'Aliases (relative={self.relative}):')
|
||||
for original_pattern, regex, result in self.aliases:
|
||||
self.debugfn(f" Rule: {original_pattern!r} -> {result!r} using regex {regex.pattern!r}")
|
||||
self.debugfn(f' Rule: {original_pattern!r} -> {result!r} using regex {regex.pattern!r}')
|
||||
|
||||
def add(self, pattern: str, result: str) -> None:
|
||||
"""Add the `pattern`/`result` pair to the list of aliases.
|
||||
|
|
@ -421,16 +429,16 @@ class PathAliases:
|
|||
pattern_sep = sep(pattern)
|
||||
|
||||
if len(pattern) > 1:
|
||||
pattern = pattern.rstrip(r"\/")
|
||||
pattern = pattern.rstrip(r'\/')
|
||||
|
||||
# The pattern can't end with a wildcard component.
|
||||
if pattern.endswith("*"):
|
||||
raise ConfigError("Pattern must not end with wildcards.")
|
||||
if pattern.endswith('*'):
|
||||
raise ConfigError('Pattern must not end with wildcards.')
|
||||
|
||||
# The pattern is meant to match a file path. Let's make it absolute
|
||||
# unless it already is, or is meant to match any prefix.
|
||||
if not self.relative:
|
||||
if not pattern.startswith("*") and not isabs_anywhere(pattern + pattern_sep):
|
||||
if not pattern.startswith('*') and not isabs_anywhere(pattern + pattern_sep):
|
||||
pattern = abs_file(pattern)
|
||||
if not pattern.endswith(pattern_sep):
|
||||
pattern += pattern_sep
|
||||
|
|
@ -440,10 +448,10 @@ class PathAliases:
|
|||
|
||||
# Normalize the result: it must end with a path separator.
|
||||
result_sep = sep(result)
|
||||
result = result.rstrip(r"\/") + result_sep
|
||||
result = result.rstrip(r'\/') + result_sep
|
||||
self.aliases.append((original_pattern, regex, result))
|
||||
|
||||
def map(self, path: str, exists:Callable[[str], bool] = source_exists) -> str:
|
||||
def map(self, path: str, exists: Callable[[str], bool] = source_exists) -> str:
|
||||
"""Map `path` through the aliases.
|
||||
|
||||
`path` is checked against all of the patterns. The first pattern to
|
||||
|
|
@ -472,18 +480,18 @@ class PathAliases:
|
|||
new = new.replace(sep(path), sep(result))
|
||||
if not self.relative:
|
||||
new = canonical_filename(new)
|
||||
dot_start = result.startswith(("./", ".\\")) and len(result) > 2
|
||||
if new.startswith(("./", ".\\")) and not dot_start:
|
||||
dot_start = result.startswith(('./', '.\\')) and len(result) > 2
|
||||
if new.startswith(('./', '.\\')) and not dot_start:
|
||||
new = new[2:]
|
||||
if not exists(new):
|
||||
self.debugfn(
|
||||
f"Rule {original_pattern!r} changed {path!r} to {new!r} " +
|
||||
f'Rule {original_pattern!r} changed {path!r} to {new!r} ' +
|
||||
"which doesn't exist, continuing",
|
||||
)
|
||||
continue
|
||||
self.debugfn(
|
||||
f"Matched path {path!r} to rule {original_pattern!r} -> {result!r}, " +
|
||||
f"producing {new!r}",
|
||||
f'Matched path {path!r} to rule {original_pattern!r} -> {result!r}, ' +
|
||||
f'producing {new!r}',
|
||||
)
|
||||
return new
|
||||
|
||||
|
|
@ -494,21 +502,21 @@ class PathAliases:
|
|||
|
||||
if self.relative and not isabs_anywhere(path):
|
||||
# Auto-generate a pattern to implicitly match relative files
|
||||
parts = re.split(r"[/\\]", path)
|
||||
parts = re.split(r'[/\\]', path)
|
||||
if len(parts) > 1:
|
||||
dir1 = parts[0]
|
||||
pattern = f"*/{dir1}"
|
||||
regex_pat = fr"^(.*[\\/])?{re.escape(dir1)}[\\/]"
|
||||
result = f"{dir1}{os.sep}"
|
||||
pattern = f'*/{dir1}'
|
||||
regex_pat = fr'^(.*[\\/])?{re.escape(dir1)}[\\/]'
|
||||
result = f'{dir1}{os.sep}'
|
||||
# Only add a new pattern if we don't already have this pattern.
|
||||
if not any(p == pattern for p, _, _ in self.aliases):
|
||||
self.debugfn(
|
||||
f"Generating rule: {pattern!r} -> {result!r} using regex {regex_pat!r}",
|
||||
f'Generating rule: {pattern!r} -> {result!r} using regex {regex_pat!r}',
|
||||
)
|
||||
self.aliases.append((pattern, re.compile(regex_pat), result))
|
||||
return self.map(path, exists=exists)
|
||||
|
||||
self.debugfn(f"No rules match, path {path!r} is unchanged")
|
||||
self.debugfn(f'No rules match, path {path!r} is unchanged')
|
||||
return path
|
||||
|
||||
|
||||
|
|
@ -530,7 +538,7 @@ def find_python_files(dirname: str, include_namespace_packages: bool) -> Iterabl
|
|||
"""
|
||||
for i, (dirpath, dirnames, filenames) in enumerate(os.walk(dirname)):
|
||||
if not include_namespace_packages:
|
||||
if i > 0 and "__init__.py" not in filenames:
|
||||
if i > 0 and '__init__.py' not in filenames:
|
||||
# If a directory doesn't have __init__.py, then it isn't
|
||||
# importable and neither are its files
|
||||
del dirnames[:]
|
||||
|
|
@ -539,7 +547,7 @@ def find_python_files(dirname: str, include_namespace_packages: bool) -> Iterabl
|
|||
# We're only interested in files that look like reasonable Python
|
||||
# files: Must end with .py or .pyw, and must not have certain funny
|
||||
# characters that probably mean they are editor junk.
|
||||
if re.match(r"^[^.#~!$@%^&*()+=,]+\.pyw?$", filename):
|
||||
if re.match(r'^[^.#~!$@%^&*()+=,]+\.pyw?$', filename):
|
||||
yield os.path.join(dirpath, filename)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""HTML reporting for coverage.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
|
|
@ -13,20 +11,31 @@ import os
|
|||
import re
|
||||
import shutil
|
||||
import string
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Iterable, TYPE_CHECKING, cast
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import coverage
|
||||
from coverage.data import CoverageData, add_data_to_hash
|
||||
from coverage.data import add_data_to_hash
|
||||
from coverage.data import CoverageData
|
||||
from coverage.exceptions import NoDataError
|
||||
from coverage.files import flat_rootname
|
||||
from coverage.misc import ensure_dir, file_be_gone, Hasher, isolate_module, format_local_datetime
|
||||
from coverage.misc import human_sorted, plural, stdout_link
|
||||
from coverage.misc import ensure_dir
|
||||
from coverage.misc import file_be_gone
|
||||
from coverage.misc import format_local_datetime
|
||||
from coverage.misc import Hasher
|
||||
from coverage.misc import human_sorted
|
||||
from coverage.misc import isolate_module
|
||||
from coverage.misc import plural
|
||||
from coverage.misc import stdout_link
|
||||
from coverage.report_core import get_analysis_to_report
|
||||
from coverage.results import Analysis, Numbers
|
||||
from coverage.results import Analysis
|
||||
from coverage.results import Numbers
|
||||
from coverage.templite import Templite
|
||||
from coverage.types import TLineNo, TMorf
|
||||
from coverage.types import TLineNo
|
||||
from coverage.types import TMorf
|
||||
from coverage.version import __url__
|
||||
|
||||
|
||||
|
|
@ -56,7 +65,7 @@ os = isolate_module(os)
|
|||
def data_filename(fname: str) -> str:
|
||||
"""Return the path to an "htmlfiles" data file of ours.
|
||||
"""
|
||||
static_dir = os.path.join(os.path.dirname(__file__), "htmlfiles")
|
||||
static_dir = os.path.join(os.path.dirname(__file__), 'htmlfiles')
|
||||
static_filename = os.path.join(static_dir, fname)
|
||||
return static_filename
|
||||
|
||||
|
|
@ -69,9 +78,9 @@ def read_data(fname: str) -> str:
|
|||
|
||||
def write_html(fname: str, html: str) -> None:
|
||||
"""Write `html` to `fname`, properly encoded."""
|
||||
html = re.sub(r"(\A\s+)|(\s+$)", "", html, flags=re.MULTILINE) + "\n"
|
||||
with open(fname, "wb") as fout:
|
||||
fout.write(html.encode("ascii", "xmlcharrefreplace"))
|
||||
html = re.sub(r'(\A\s+)|(\s+$)', '', html, flags=re.MULTILINE) + '\n'
|
||||
with open(fname, 'wb') as fout:
|
||||
fout.write(html.encode('ascii', 'xmlcharrefreplace'))
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -86,11 +95,11 @@ class LineData:
|
|||
context_list: list[str]
|
||||
short_annotations: list[str]
|
||||
long_annotations: list[str]
|
||||
html: str = ""
|
||||
html: str = ''
|
||||
context_str: str | None = None
|
||||
annotate: str | None = None
|
||||
annotate_long: str | None = None
|
||||
css_class: str = ""
|
||||
css_class: str = ''
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -104,7 +113,7 @@ class FileData:
|
|||
class HtmlDataGeneration:
|
||||
"""Generate structured data to be turned into HTML reports."""
|
||||
|
||||
EMPTY = "(empty)"
|
||||
EMPTY = '(empty)'
|
||||
|
||||
def __init__(self, cov: Coverage) -> None:
|
||||
self.coverage = cov
|
||||
|
|
@ -112,8 +121,8 @@ class HtmlDataGeneration:
|
|||
data = self.coverage.get_data()
|
||||
self.has_arcs = data.has_arcs()
|
||||
if self.config.show_contexts:
|
||||
if data.measured_contexts() == {""}:
|
||||
self.coverage._warn("No contexts were measured")
|
||||
if data.measured_contexts() == {''}:
|
||||
self.coverage._warn('No contexts were measured')
|
||||
data.set_query_contexts(self.config.report_contexts)
|
||||
|
||||
def data_for_file(self, fr: FileReporter, analysis: Analysis) -> FileData:
|
||||
|
|
@ -129,47 +138,49 @@ class HtmlDataGeneration:
|
|||
|
||||
for lineno, tokens in enumerate(fr.source_token_lines(), start=1):
|
||||
# Figure out how to mark this line.
|
||||
category = ""
|
||||
category = ''
|
||||
short_annotations = []
|
||||
long_annotations = []
|
||||
|
||||
if lineno in analysis.excluded:
|
||||
category = "exc"
|
||||
category = 'exc'
|
||||
elif lineno in analysis.missing:
|
||||
category = "mis"
|
||||
category = 'mis'
|
||||
elif self.has_arcs and lineno in missing_branch_arcs:
|
||||
category = "par"
|
||||
category = 'par'
|
||||
for b in missing_branch_arcs[lineno]:
|
||||
if b < 0:
|
||||
short_annotations.append("exit")
|
||||
short_annotations.append('exit')
|
||||
else:
|
||||
short_annotations.append(str(b))
|
||||
long_annotations.append(fr.missing_arc_description(lineno, b, arcs_executed))
|
||||
elif lineno in analysis.statements:
|
||||
category = "run"
|
||||
category = 'run'
|
||||
|
||||
contexts = []
|
||||
contexts_label = ""
|
||||
contexts_label = ''
|
||||
context_list = []
|
||||
if category and self.config.show_contexts:
|
||||
contexts = human_sorted(c or self.EMPTY for c in contexts_by_lineno.get(lineno, ()))
|
||||
if contexts == [self.EMPTY]:
|
||||
contexts_label = self.EMPTY
|
||||
else:
|
||||
contexts_label = f"{len(contexts)} ctx"
|
||||
contexts_label = f'{len(contexts)} ctx'
|
||||
context_list = contexts
|
||||
|
||||
lines.append(LineData(
|
||||
tokens=tokens,
|
||||
number=lineno,
|
||||
category=category,
|
||||
statement=(lineno in analysis.statements),
|
||||
contexts=contexts,
|
||||
contexts_label=contexts_label,
|
||||
context_list=context_list,
|
||||
short_annotations=short_annotations,
|
||||
long_annotations=long_annotations,
|
||||
))
|
||||
lines.append(
|
||||
LineData(
|
||||
tokens=tokens,
|
||||
number=lineno,
|
||||
category=category,
|
||||
statement=(lineno in analysis.statements),
|
||||
contexts=contexts,
|
||||
contexts_label=contexts_label,
|
||||
context_list=context_list,
|
||||
short_annotations=short_annotations,
|
||||
long_annotations=long_annotations,
|
||||
),
|
||||
)
|
||||
|
||||
file_data = FileData(
|
||||
relative_filename=fr.relative_filename(),
|
||||
|
|
@ -182,15 +193,17 @@ class HtmlDataGeneration:
|
|||
|
||||
class FileToReport:
|
||||
"""A file we're considering reporting."""
|
||||
|
||||
def __init__(self, fr: FileReporter, analysis: Analysis) -> None:
|
||||
self.fr = fr
|
||||
self.analysis = analysis
|
||||
self.rootname = flat_rootname(fr.relative_filename())
|
||||
self.html_filename = self.rootname + ".html"
|
||||
self.html_filename = self.rootname + '.html'
|
||||
|
||||
|
||||
HTML_SAFE = string.ascii_letters + string.digits + "!#$%'()*+,-./:;=?@[]^_`{|}~"
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def encode_int(n: int) -> str:
|
||||
"""Create a short HTML-safe string from an integer, using HTML_SAFE."""
|
||||
|
|
@ -201,7 +214,7 @@ def encode_int(n: int) -> str:
|
|||
while n:
|
||||
n, t = divmod(n, len(HTML_SAFE))
|
||||
r.append(HTML_SAFE[t])
|
||||
return "".join(r)
|
||||
return ''.join(r)
|
||||
|
||||
|
||||
class HtmlReporter:
|
||||
|
|
@ -210,11 +223,11 @@ class HtmlReporter:
|
|||
# These files will be copied from the htmlfiles directory to the output
|
||||
# directory.
|
||||
STATIC_FILES = [
|
||||
"style.css",
|
||||
"coverage_html.js",
|
||||
"keybd_closed.png",
|
||||
"keybd_open.png",
|
||||
"favicon_32.png",
|
||||
'style.css',
|
||||
'coverage_html.js',
|
||||
'keybd_closed.png',
|
||||
'keybd_open.png',
|
||||
'favicon_32.png',
|
||||
]
|
||||
|
||||
def __init__(self, cov: Coverage) -> None:
|
||||
|
|
@ -253,29 +266,29 @@ class HtmlReporter:
|
|||
|
||||
self.template_globals = {
|
||||
# Functions available in the templates.
|
||||
"escape": escape,
|
||||
"pair": pair,
|
||||
"len": len,
|
||||
'escape': escape,
|
||||
'pair': pair,
|
||||
'len': len,
|
||||
|
||||
# Constants for this report.
|
||||
"__url__": __url__,
|
||||
"__version__": coverage.__version__,
|
||||
"title": title,
|
||||
"time_stamp": format_local_datetime(datetime.datetime.now()),
|
||||
"extra_css": self.extra_css,
|
||||
"has_arcs": self.has_arcs,
|
||||
"show_contexts": self.config.show_contexts,
|
||||
'__url__': __url__,
|
||||
'__version__': coverage.__version__,
|
||||
'title': title,
|
||||
'time_stamp': format_local_datetime(datetime.datetime.now()),
|
||||
'extra_css': self.extra_css,
|
||||
'has_arcs': self.has_arcs,
|
||||
'show_contexts': self.config.show_contexts,
|
||||
|
||||
# Constants for all reports.
|
||||
# These css classes determine which lines are highlighted by default.
|
||||
"category": {
|
||||
"exc": "exc show_exc",
|
||||
"mis": "mis show_mis",
|
||||
"par": "par run show_par",
|
||||
"run": "run",
|
||||
'category': {
|
||||
'exc': 'exc show_exc',
|
||||
'mis': 'mis show_mis',
|
||||
'par': 'par run show_par',
|
||||
'run': 'run',
|
||||
},
|
||||
}
|
||||
self.pyfile_html_source = read_data("pyfile.html")
|
||||
self.pyfile_html_source = read_data('pyfile.html')
|
||||
self.source_tmpl = Templite(self.pyfile_html_source, self.template_globals)
|
||||
|
||||
def report(self, morfs: Iterable[TMorf] | None) -> float:
|
||||
|
|
@ -303,17 +316,17 @@ class HtmlReporter:
|
|||
|
||||
for i, ftr in enumerate(files_to_report):
|
||||
if i == 0:
|
||||
prev_html = "index.html"
|
||||
prev_html = 'index.html'
|
||||
else:
|
||||
prev_html = files_to_report[i - 1].html_filename
|
||||
if i == len(files_to_report) - 1:
|
||||
next_html = "index.html"
|
||||
next_html = 'index.html'
|
||||
else:
|
||||
next_html = files_to_report[i + 1].html_filename
|
||||
self.write_html_file(ftr, prev_html, next_html)
|
||||
|
||||
if not self.all_files_nums:
|
||||
raise NoDataError("No data to report.")
|
||||
raise NoDataError('No data to report.')
|
||||
|
||||
self.totals = cast(Numbers, sum(self.all_files_nums))
|
||||
|
||||
|
|
@ -322,7 +335,7 @@ class HtmlReporter:
|
|||
first_html = files_to_report[0].html_filename
|
||||
final_html = files_to_report[-1].html_filename
|
||||
else:
|
||||
first_html = final_html = "index.html"
|
||||
first_html = final_html = 'index.html'
|
||||
self.index_file(first_html, final_html)
|
||||
|
||||
self.make_local_static_report_files()
|
||||
|
|
@ -344,8 +357,8 @@ class HtmlReporter:
|
|||
# .gitignore can't be copied from the source tree because it would
|
||||
# prevent 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")
|
||||
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:
|
||||
|
|
@ -401,29 +414,29 @@ class HtmlReporter:
|
|||
# Build the HTML for the line.
|
||||
html_parts = []
|
||||
for tok_type, tok_text in ldata.tokens:
|
||||
if tok_type == "ws":
|
||||
if tok_type == 'ws':
|
||||
html_parts.append(escape(tok_text))
|
||||
else:
|
||||
tok_html = escape(tok_text) or " "
|
||||
tok_html = escape(tok_text) or ' '
|
||||
html_parts.append(f'<span class="{tok_type}">{tok_html}</span>')
|
||||
ldata.html = "".join(html_parts)
|
||||
ldata.html = ''.join(html_parts)
|
||||
if ldata.context_list:
|
||||
encoded_contexts = [
|
||||
encode_int(context_codes[c_context]) for c_context in ldata.context_list
|
||||
]
|
||||
code_width = max(len(ec) for ec in encoded_contexts)
|
||||
ldata.context_str = (
|
||||
str(code_width)
|
||||
+ "".join(ec.ljust(code_width) for ec in encoded_contexts)
|
||||
str(code_width) +
|
||||
''.join(ec.ljust(code_width) for ec in encoded_contexts)
|
||||
)
|
||||
else:
|
||||
ldata.context_str = ""
|
||||
ldata.context_str = ''
|
||||
|
||||
if ldata.short_annotations:
|
||||
# 202F is NARROW NO-BREAK SPACE.
|
||||
# 219B is RIGHTWARDS ARROW WITH STROKE.
|
||||
ldata.annotate = ", ".join(
|
||||
f"{ldata.number} ↛ {d}"
|
||||
ldata.annotate = ', '.join(
|
||||
f'{ldata.number} ↛ {d}'
|
||||
for d in ldata.short_annotations
|
||||
)
|
||||
else:
|
||||
|
|
@ -434,10 +447,10 @@ class HtmlReporter:
|
|||
if len(longs) == 1:
|
||||
ldata.annotate_long = longs[0]
|
||||
else:
|
||||
ldata.annotate_long = "{:d} missed branches: {}".format(
|
||||
ldata.annotate_long = '{:d} missed branches: {}'.format(
|
||||
len(longs),
|
||||
", ".join(
|
||||
f"{num:d}) {ann_long}"
|
||||
', '.join(
|
||||
f'{num:d}) {ann_long}'
|
||||
for num, ann_long in enumerate(longs, start=1)
|
||||
),
|
||||
)
|
||||
|
|
@ -447,24 +460,24 @@ class HtmlReporter:
|
|||
css_classes = []
|
||||
if ldata.category:
|
||||
css_classes.append(
|
||||
self.template_globals["category"][ldata.category], # type: ignore[index]
|
||||
self.template_globals['category'][ldata.category], # type: ignore[index]
|
||||
)
|
||||
ldata.css_class = " ".join(css_classes) or "pln"
|
||||
ldata.css_class = ' '.join(css_classes) or 'pln'
|
||||
|
||||
html_path = os.path.join(self.directory, ftr.html_filename)
|
||||
html = self.source_tmpl.render({
|
||||
**file_data.__dict__,
|
||||
"contexts_json": contexts_json,
|
||||
"prev_html": prev_html,
|
||||
"next_html": next_html,
|
||||
'contexts_json': contexts_json,
|
||||
'prev_html': prev_html,
|
||||
'next_html': next_html,
|
||||
})
|
||||
write_html(html_path, html)
|
||||
|
||||
# Save this file's information for the index file.
|
||||
index_info: IndexInfoDict = {
|
||||
"nums": ftr.analysis.numbers,
|
||||
"html_filename": ftr.html_filename,
|
||||
"relative_filename": ftr.fr.relative_filename(),
|
||||
'nums': ftr.analysis.numbers,
|
||||
'html_filename': ftr.html_filename,
|
||||
'relative_filename': ftr.fr.relative_filename(),
|
||||
}
|
||||
self.file_summaries.append(index_info)
|
||||
self.incr.set_index_info(ftr.rootname, index_info)
|
||||
|
|
@ -472,30 +485,30 @@ class HtmlReporter:
|
|||
def index_file(self, first_html: str, final_html: str) -> None:
|
||||
"""Write the index.html file for this report."""
|
||||
self.make_directory()
|
||||
index_tmpl = Templite(read_data("index.html"), self.template_globals)
|
||||
index_tmpl = Templite(read_data('index.html'), self.template_globals)
|
||||
|
||||
skipped_covered_msg = skipped_empty_msg = ""
|
||||
skipped_covered_msg = skipped_empty_msg = ''
|
||||
if self.skipped_covered_count:
|
||||
n = self.skipped_covered_count
|
||||
skipped_covered_msg = f"{n} file{plural(n)} skipped due to complete coverage."
|
||||
skipped_covered_msg = f'{n} file{plural(n)} skipped due to complete coverage.'
|
||||
if self.skipped_empty_count:
|
||||
n = self.skipped_empty_count
|
||||
skipped_empty_msg = f"{n} empty file{plural(n)} skipped."
|
||||
skipped_empty_msg = f'{n} empty file{plural(n)} skipped.'
|
||||
|
||||
html = index_tmpl.render({
|
||||
"files": self.file_summaries,
|
||||
"totals": self.totals,
|
||||
"skipped_covered_msg": skipped_covered_msg,
|
||||
"skipped_empty_msg": skipped_empty_msg,
|
||||
"first_html": first_html,
|
||||
"final_html": final_html,
|
||||
'files': self.file_summaries,
|
||||
'totals': self.totals,
|
||||
'skipped_covered_msg': skipped_covered_msg,
|
||||
'skipped_empty_msg': skipped_empty_msg,
|
||||
'first_html': first_html,
|
||||
'final_html': final_html,
|
||||
})
|
||||
|
||||
index_file = os.path.join(self.directory, "index.html")
|
||||
index_file = os.path.join(self.directory, 'index.html')
|
||||
write_html(index_file, html)
|
||||
|
||||
print_href = stdout_link(index_file, f"file://{os.path.abspath(index_file)}")
|
||||
self.coverage._message(f"Wrote HTML report to {print_href}")
|
||||
print_href = stdout_link(index_file, f'file://{os.path.abspath(index_file)}')
|
||||
self.coverage._message(f'Wrote HTML report to {print_href}')
|
||||
|
||||
# Write the latest hashes for next time.
|
||||
self.incr.write()
|
||||
|
|
@ -504,12 +517,12 @@ class HtmlReporter:
|
|||
class IncrementalChecker:
|
||||
"""Logic and data to support incremental reporting."""
|
||||
|
||||
STATUS_FILE = "status.json"
|
||||
STATUS_FILE = 'status.json'
|
||||
STATUS_FORMAT = 2
|
||||
NOTE = (
|
||||
"This file is an internal implementation detail to speed up HTML report"
|
||||
+ " generation. Its format can change at any time. You might be looking"
|
||||
+ " for the JSON report: https://coverage.rtfd.io/cmd.html#cmd-json"
|
||||
'This file is an internal implementation detail to speed up HTML report' +
|
||||
' generation. Its format can change at any time. You might be looking' +
|
||||
' for the JSON report: https://coverage.rtfd.io/cmd.html#cmd-json'
|
||||
)
|
||||
|
||||
# The data looks like:
|
||||
|
|
@ -545,7 +558,7 @@ class IncrementalChecker:
|
|||
|
||||
def reset(self) -> None:
|
||||
"""Initialize to empty. Causes all files to be reported."""
|
||||
self.globals = ""
|
||||
self.globals = ''
|
||||
self.files: dict[str, FileInfoDict] = {}
|
||||
|
||||
def read(self) -> None:
|
||||
|
|
@ -559,17 +572,17 @@ class IncrementalChecker:
|
|||
usable = False
|
||||
else:
|
||||
usable = True
|
||||
if status["format"] != self.STATUS_FORMAT:
|
||||
if status['format'] != self.STATUS_FORMAT:
|
||||
usable = False
|
||||
elif status["version"] != coverage.__version__:
|
||||
elif status['version'] != coverage.__version__:
|
||||
usable = False
|
||||
|
||||
if usable:
|
||||
self.files = {}
|
||||
for filename, fileinfo in status["files"].items():
|
||||
fileinfo["index"]["nums"] = Numbers(*fileinfo["index"]["nums"])
|
||||
for filename, fileinfo in status['files'].items():
|
||||
fileinfo['index']['nums'] = Numbers(*fileinfo['index']['nums'])
|
||||
self.files[filename] = fileinfo
|
||||
self.globals = status["globals"]
|
||||
self.globals = status['globals']
|
||||
else:
|
||||
self.reset()
|
||||
|
||||
|
|
@ -578,19 +591,19 @@ class IncrementalChecker:
|
|||
status_file = os.path.join(self.directory, self.STATUS_FILE)
|
||||
files = {}
|
||||
for filename, fileinfo in self.files.items():
|
||||
index = fileinfo["index"]
|
||||
index["nums"] = index["nums"].init_args() # type: ignore[typeddict-item]
|
||||
index = fileinfo['index']
|
||||
index['nums'] = index['nums'].init_args() # type: ignore[typeddict-item]
|
||||
files[filename] = fileinfo
|
||||
|
||||
status = {
|
||||
"note": self.NOTE,
|
||||
"format": self.STATUS_FORMAT,
|
||||
"version": coverage.__version__,
|
||||
"globals": self.globals,
|
||||
"files": files,
|
||||
'note': self.NOTE,
|
||||
'format': self.STATUS_FORMAT,
|
||||
'version': coverage.__version__,
|
||||
'globals': self.globals,
|
||||
'files': files,
|
||||
}
|
||||
with open(status_file, "w") as fout:
|
||||
json.dump(status, fout, separators=(",", ":"))
|
||||
with open(status_file, 'w') as fout:
|
||||
json.dump(status, fout, separators=(',', ':'))
|
||||
|
||||
def check_global_data(self, *data: Any) -> None:
|
||||
"""Check the global data that can affect incremental reporting."""
|
||||
|
|
@ -609,7 +622,7 @@ class IncrementalChecker:
|
|||
`rootname` is the name being used for the file.
|
||||
"""
|
||||
m = Hasher()
|
||||
m.update(fr.source().encode("utf-8"))
|
||||
m.update(fr.source().encode('utf-8'))
|
||||
add_data_to_hash(data, fr.filename, m)
|
||||
this_hash = m.hexdigest()
|
||||
|
||||
|
|
@ -624,19 +637,19 @@ class IncrementalChecker:
|
|||
|
||||
def file_hash(self, fname: str) -> str:
|
||||
"""Get the hash of `fname`'s contents."""
|
||||
return self.files.get(fname, {}).get("hash", "") # type: ignore[call-overload]
|
||||
return self.files.get(fname, {}).get('hash', '') # type: ignore[call-overload]
|
||||
|
||||
def set_file_hash(self, fname: str, val: str) -> None:
|
||||
"""Set the hash of `fname`'s contents."""
|
||||
self.files.setdefault(fname, {})["hash"] = val # type: ignore[typeddict-item]
|
||||
self.files.setdefault(fname, {})['hash'] = val # type: ignore[typeddict-item]
|
||||
|
||||
def index_info(self, fname: str) -> IndexInfoDict:
|
||||
"""Get the information for index.html for `fname`."""
|
||||
return self.files.get(fname, {}).get("index", {}) # type: ignore
|
||||
return self.files.get(fname, {}).get('index', {}) # type: ignore
|
||||
|
||||
def set_index_info(self, fname: str, info: IndexInfoDict) -> None:
|
||||
"""Set the information for index.html for `fname`."""
|
||||
self.files.setdefault(fname, {})["index"] = info # type: ignore[typeddict-item]
|
||||
self.files.setdefault(fname, {})['index'] = info # type: ignore[typeddict-item]
|
||||
|
||||
|
||||
# Helpers for templates and generating HTML
|
||||
|
|
@ -648,9 +661,9 @@ def escape(t: str) -> str:
|
|||
|
||||
"""
|
||||
# Convert HTML special chars into HTML entities.
|
||||
return t.replace("&", "&").replace("<", "<")
|
||||
return t.replace('&', '&').replace('<', '<')
|
||||
|
||||
|
||||
def pair(ratio: tuple[int, int]) -> str:
|
||||
"""Format a pair of numbers so JavaScript can read them in an attribute."""
|
||||
return "{} {}".format(*ratio)
|
||||
return '{} {}'.format(*ratio)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Determining whether files are being measured/reported or not."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
|
|
@ -14,20 +12,31 @@ import re
|
|||
import sys
|
||||
import sysconfig
|
||||
import traceback
|
||||
|
||||
from types import FrameType, ModuleType
|
||||
from typing import (
|
||||
cast, Any, Iterable, TYPE_CHECKING,
|
||||
)
|
||||
from types import FrameType
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from coverage import env
|
||||
from coverage.disposition import FileDisposition, disposition_init
|
||||
from coverage.exceptions import CoverageException, PluginError
|
||||
from coverage.files import TreeMatcher, GlobMatcher, ModuleMatcher
|
||||
from coverage.files import prep_patterns, find_python_files, canonical_filename
|
||||
from coverage.disposition import disposition_init
|
||||
from coverage.disposition import FileDisposition
|
||||
from coverage.exceptions import CoverageException
|
||||
from coverage.exceptions import PluginError
|
||||
from coverage.files import canonical_filename
|
||||
from coverage.files import find_python_files
|
||||
from coverage.files import GlobMatcher
|
||||
from coverage.files import ModuleMatcher
|
||||
from coverage.files import prep_patterns
|
||||
from coverage.files import TreeMatcher
|
||||
from coverage.misc import sys_modules_saved
|
||||
from coverage.python import source_for_file, source_for_morf
|
||||
from coverage.types import TFileDisposition, TMorf, TWarnFn, TDebugCtl
|
||||
from coverage.python import source_for_file
|
||||
from coverage.python import source_for_morf
|
||||
from coverage.types import TDebugCtl
|
||||
from coverage.types import TFileDisposition
|
||||
from coverage.types import TMorf
|
||||
from coverage.types import TWarnFn
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from coverage.config import CoverageConfig
|
||||
|
|
@ -65,7 +74,7 @@ def canonical_path(morf: TMorf, directory: bool = False) -> str:
|
|||
|
||||
"""
|
||||
morf_path = canonical_filename(source_for_morf(morf))
|
||||
if morf_path.endswith("__init__.py") or directory:
|
||||
if morf_path.endswith('__init__.py') or directory:
|
||||
morf_path = os.path.split(morf_path)[0]
|
||||
return morf_path
|
||||
|
||||
|
|
@ -83,16 +92,16 @@ def name_for_module(filename: str, frame: FrameType | None) -> str:
|
|||
|
||||
"""
|
||||
module_globals = frame.f_globals if frame is not None else {}
|
||||
dunder_name: str = module_globals.get("__name__", None)
|
||||
dunder_name: str = module_globals.get('__name__', None)
|
||||
|
||||
if isinstance(dunder_name, str) and dunder_name != "__main__":
|
||||
if isinstance(dunder_name, str) and dunder_name != '__main__':
|
||||
# This is the usual case: an imported module.
|
||||
return dunder_name
|
||||
|
||||
spec = module_globals.get("__spec__", None)
|
||||
spec = module_globals.get('__spec__', None)
|
||||
if spec:
|
||||
fullname = spec.name
|
||||
if isinstance(fullname, str) and fullname != "__main__":
|
||||
if isinstance(fullname, str) and fullname != '__main__':
|
||||
# Module loaded via: runpy -m
|
||||
return fullname
|
||||
|
||||
|
|
@ -106,12 +115,12 @@ def name_for_module(filename: str, frame: FrameType | None) -> str:
|
|||
|
||||
def module_is_namespace(mod: ModuleType) -> bool:
|
||||
"""Is the module object `mod` a PEP420 namespace module?"""
|
||||
return hasattr(mod, "__path__") and getattr(mod, "__file__", None) is None
|
||||
return hasattr(mod, '__path__') and getattr(mod, '__file__', None) is None
|
||||
|
||||
|
||||
def module_has_file(mod: ModuleType) -> bool:
|
||||
"""Does the module object `mod` have an existing __file__ ?"""
|
||||
mod__file__ = getattr(mod, "__file__", None)
|
||||
mod__file__ = getattr(mod, '__file__', None)
|
||||
if mod__file__ is None:
|
||||
return False
|
||||
return os.path.exists(mod__file__)
|
||||
|
|
@ -146,7 +155,7 @@ def add_stdlib_paths(paths: set[str]) -> None:
|
|||
# spread across a few locations. Look at all the candidate modules
|
||||
# we've imported, and take all the different ones.
|
||||
for m in modules_we_happen_to_have:
|
||||
if hasattr(m, "__file__"):
|
||||
if hasattr(m, '__file__'):
|
||||
paths.add(canonical_path(m, directory=True))
|
||||
|
||||
|
||||
|
|
@ -157,10 +166,10 @@ def add_third_party_paths(paths: set[str]) -> None:
|
|||
|
||||
for scheme in scheme_names:
|
||||
# https://foss.heptapod.net/pypy/pypy/-/issues/3433
|
||||
better_scheme = "pypy_posix" if scheme == "pypy" else scheme
|
||||
if os.name in better_scheme.split("_"):
|
||||
better_scheme = 'pypy_posix' if scheme == 'pypy' else scheme
|
||||
if os.name in better_scheme.split('_'):
|
||||
config_paths = sysconfig.get_paths(scheme)
|
||||
for path_name in ["platlib", "purelib", "scripts"]:
|
||||
for path_name in ['platlib', 'purelib', 'scripts']:
|
||||
paths.add(config_paths[path_name])
|
||||
|
||||
|
||||
|
|
@ -170,7 +179,7 @@ def add_coverage_paths(paths: set[str]) -> None:
|
|||
paths.add(cover_path)
|
||||
if env.TESTING:
|
||||
# Don't include our own test code.
|
||||
paths.add(os.path.join(cover_path, "tests"))
|
||||
paths.add(os.path.join(cover_path, 'tests'))
|
||||
|
||||
|
||||
class InOrOut:
|
||||
|
|
@ -221,7 +230,7 @@ class InOrOut:
|
|||
# The matchers for should_trace.
|
||||
|
||||
# Generally useful information
|
||||
_debug("sys.path:" + "".join(f"\n {p}" for p in sys.path))
|
||||
_debug('sys.path:' + ''.join(f'\n {p}' for p in sys.path))
|
||||
|
||||
# Create the matchers we need for should_trace
|
||||
self.source_match = None
|
||||
|
|
@ -232,28 +241,28 @@ class InOrOut:
|
|||
if self.source or self.source_pkgs:
|
||||
against = []
|
||||
if self.source:
|
||||
self.source_match = TreeMatcher(self.source, "source")
|
||||
against.append(f"trees {self.source_match!r}")
|
||||
self.source_match = TreeMatcher(self.source, 'source')
|
||||
against.append(f'trees {self.source_match!r}')
|
||||
if self.source_pkgs:
|
||||
self.source_pkgs_match = ModuleMatcher(self.source_pkgs, "source_pkgs")
|
||||
against.append(f"modules {self.source_pkgs_match!r}")
|
||||
_debug("Source matching against " + " and ".join(against))
|
||||
self.source_pkgs_match = ModuleMatcher(self.source_pkgs, 'source_pkgs')
|
||||
against.append(f'modules {self.source_pkgs_match!r}')
|
||||
_debug('Source matching against ' + ' and '.join(against))
|
||||
else:
|
||||
if self.pylib_paths:
|
||||
self.pylib_match = TreeMatcher(self.pylib_paths, "pylib")
|
||||
_debug(f"Python stdlib matching: {self.pylib_match!r}")
|
||||
self.pylib_match = TreeMatcher(self.pylib_paths, 'pylib')
|
||||
_debug(f'Python stdlib matching: {self.pylib_match!r}')
|
||||
if self.include:
|
||||
self.include_match = GlobMatcher(self.include, "include")
|
||||
_debug(f"Include matching: {self.include_match!r}")
|
||||
self.include_match = GlobMatcher(self.include, 'include')
|
||||
_debug(f'Include matching: {self.include_match!r}')
|
||||
if self.omit:
|
||||
self.omit_match = GlobMatcher(self.omit, "omit")
|
||||
_debug(f"Omit matching: {self.omit_match!r}")
|
||||
self.omit_match = GlobMatcher(self.omit, 'omit')
|
||||
_debug(f'Omit matching: {self.omit_match!r}')
|
||||
|
||||
self.cover_match = TreeMatcher(self.cover_paths, "coverage")
|
||||
_debug(f"Coverage code matching: {self.cover_match!r}")
|
||||
self.cover_match = TreeMatcher(self.cover_paths, 'coverage')
|
||||
_debug(f'Coverage code matching: {self.cover_match!r}')
|
||||
|
||||
self.third_match = TreeMatcher(self.third_paths, "third")
|
||||
_debug(f"Third-party lib matching: {self.third_match!r}")
|
||||
self.third_match = TreeMatcher(self.third_paths, 'third')
|
||||
_debug(f'Third-party lib matching: {self.third_match!r}')
|
||||
|
||||
# Check if the source we want to measure has been installed as a
|
||||
# third-party package.
|
||||
|
|
@ -263,30 +272,30 @@ class InOrOut:
|
|||
for pkg in self.source_pkgs:
|
||||
try:
|
||||
modfile, path = file_and_path_for_module(pkg)
|
||||
_debug(f"Imported source package {pkg!r} as {modfile!r}")
|
||||
_debug(f'Imported source package {pkg!r} as {modfile!r}')
|
||||
except CoverageException as exc:
|
||||
_debug(f"Couldn't import source package {pkg!r}: {exc}")
|
||||
continue
|
||||
if modfile:
|
||||
if self.third_match.match(modfile):
|
||||
_debug(
|
||||
f"Source in third-party: source_pkg {pkg!r} at {modfile!r}",
|
||||
f'Source in third-party: source_pkg {pkg!r} at {modfile!r}',
|
||||
)
|
||||
self.source_in_third_paths.add(canonical_path(source_for_file(modfile)))
|
||||
else:
|
||||
for pathdir in path:
|
||||
if self.third_match.match(pathdir):
|
||||
_debug(
|
||||
f"Source in third-party: {pkg!r} path directory at {pathdir!r}",
|
||||
f'Source in third-party: {pkg!r} path directory at {pathdir!r}',
|
||||
)
|
||||
self.source_in_third_paths.add(pathdir)
|
||||
|
||||
for src in self.source:
|
||||
if self.third_match.match(src):
|
||||
_debug(f"Source in third-party: source directory {src!r}")
|
||||
_debug(f'Source in third-party: source directory {src!r}')
|
||||
self.source_in_third_paths.add(src)
|
||||
self.source_in_third_match = TreeMatcher(self.source_in_third_paths, "source_in_third")
|
||||
_debug(f"Source in third-party matching: {self.source_in_third_match}")
|
||||
self.source_in_third_match = TreeMatcher(self.source_in_third_paths, 'source_in_third')
|
||||
_debug(f'Source in third-party matching: {self.source_in_third_match}')
|
||||
|
||||
self.plugins: Plugins
|
||||
self.disp_class: type[TFileDisposition] = FileDisposition
|
||||
|
|
@ -309,8 +318,8 @@ class InOrOut:
|
|||
disp.reason = reason
|
||||
return disp
|
||||
|
||||
if original_filename.startswith("<"):
|
||||
return nope(disp, "original file name is not real")
|
||||
if original_filename.startswith('<'):
|
||||
return nope(disp, 'original file name is not real')
|
||||
|
||||
if frame is not None:
|
||||
# Compiled Python files have two file names: frame.f_code.co_filename is
|
||||
|
|
@ -319,10 +328,10 @@ class InOrOut:
|
|||
# .pyc files can be moved after compilation (for example, by being
|
||||
# installed), we look for __file__ in the frame and prefer it to the
|
||||
# co_filename value.
|
||||
dunder_file = frame.f_globals and frame.f_globals.get("__file__")
|
||||
dunder_file = frame.f_globals and frame.f_globals.get('__file__')
|
||||
if dunder_file:
|
||||
filename = source_for_file(dunder_file)
|
||||
if original_filename and not original_filename.startswith("<"):
|
||||
if original_filename and not original_filename.startswith('<'):
|
||||
orig = os.path.basename(original_filename)
|
||||
if orig != os.path.basename(filename):
|
||||
# Files shouldn't be renamed when moved. This happens when
|
||||
|
|
@ -334,15 +343,15 @@ class InOrOut:
|
|||
# Empty string is pretty useless.
|
||||
return nope(disp, "empty string isn't a file name")
|
||||
|
||||
if filename.startswith("memory:"):
|
||||
if filename.startswith('memory:'):
|
||||
return nope(disp, "memory isn't traceable")
|
||||
|
||||
if filename.startswith("<"):
|
||||
if filename.startswith('<'):
|
||||
# Lots of non-file execution is represented with artificial
|
||||
# file names like "<string>", "<doctest readme.txt[0]>", or
|
||||
# "<exec_function>". Don't ever trace these executions, since we
|
||||
# can't do anything with the data later anyway.
|
||||
return nope(disp, "file name is not real")
|
||||
return nope(disp, 'file name is not real')
|
||||
|
||||
canonical = canonical_filename(filename)
|
||||
disp.canonical_filename = canonical
|
||||
|
|
@ -369,7 +378,7 @@ class InOrOut:
|
|||
except Exception:
|
||||
plugin_name = plugin._coverage_plugin_name
|
||||
tb = traceback.format_exc()
|
||||
self.warn(f"Disabling plug-in {plugin_name!r} due to an exception:\n{tb}")
|
||||
self.warn(f'Disabling plug-in {plugin_name!r} due to an exception:\n{tb}')
|
||||
plugin._coverage_enabled = False
|
||||
continue
|
||||
else:
|
||||
|
|
@ -402,7 +411,7 @@ class InOrOut:
|
|||
# any canned exclusions. If they didn't, then we have to exclude the
|
||||
# stdlib and coverage.py directories.
|
||||
if self.source_match or self.source_pkgs_match:
|
||||
extra = ""
|
||||
extra = ''
|
||||
ok = False
|
||||
if self.source_pkgs_match:
|
||||
if self.source_pkgs_match.match(modulename):
|
||||
|
|
@ -410,41 +419,41 @@ class InOrOut:
|
|||
if modulename in self.source_pkgs_unmatched:
|
||||
self.source_pkgs_unmatched.remove(modulename)
|
||||
else:
|
||||
extra = f"module {modulename!r} "
|
||||
extra = f'module {modulename!r} '
|
||||
if not ok and self.source_match:
|
||||
if self.source_match.match(filename):
|
||||
ok = True
|
||||
if not ok:
|
||||
return extra + "falls outside the --source spec"
|
||||
return extra + 'falls outside the --source spec'
|
||||
if self.third_match.match(filename) and not self.source_in_third_match.match(filename):
|
||||
return "inside --source, but is third-party"
|
||||
return 'inside --source, but is third-party'
|
||||
elif self.include_match:
|
||||
if not self.include_match.match(filename):
|
||||
return "falls outside the --include trees"
|
||||
return 'falls outside the --include trees'
|
||||
else:
|
||||
# We exclude the coverage.py code itself, since a little of it
|
||||
# will be measured otherwise.
|
||||
if self.cover_match.match(filename):
|
||||
return "is part of coverage.py"
|
||||
return 'is part of coverage.py'
|
||||
|
||||
# If we aren't supposed to trace installed code, then check if this
|
||||
# is near the Python standard library and skip it if so.
|
||||
if self.pylib_match and self.pylib_match.match(filename):
|
||||
return "is in the stdlib"
|
||||
return 'is in the stdlib'
|
||||
|
||||
# Exclude anything in the third-party installation areas.
|
||||
if self.third_match.match(filename):
|
||||
return "is a third-party module"
|
||||
return 'is a third-party module'
|
||||
|
||||
# Check the file against the omit pattern.
|
||||
if self.omit_match and self.omit_match.match(filename):
|
||||
return "is inside an --omit pattern"
|
||||
return 'is inside an --omit pattern'
|
||||
|
||||
# No point tracing a file we can't later write to SQLite.
|
||||
try:
|
||||
filename.encode("utf-8")
|
||||
filename.encode('utf-8')
|
||||
except UnicodeEncodeError:
|
||||
return "non-encodable filename"
|
||||
return 'non-encodable filename'
|
||||
|
||||
# No reason found to skip this file.
|
||||
return None
|
||||
|
|
@ -453,20 +462,20 @@ class InOrOut:
|
|||
"""Warn if there are settings that conflict."""
|
||||
if self.include:
|
||||
if self.source or self.source_pkgs:
|
||||
self.warn("--include is ignored because --source is set", slug="include-ignored")
|
||||
self.warn('--include is ignored because --source is set', slug='include-ignored')
|
||||
|
||||
def warn_already_imported_files(self) -> None:
|
||||
"""Warn if files have already been imported that we will be measuring."""
|
||||
if self.include or self.source or self.source_pkgs:
|
||||
warned = set()
|
||||
for mod in list(sys.modules.values()):
|
||||
filename = getattr(mod, "__file__", None)
|
||||
filename = getattr(mod, '__file__', None)
|
||||
if filename is None:
|
||||
continue
|
||||
if filename in warned:
|
||||
continue
|
||||
|
||||
if len(getattr(mod, "__path__", ())) > 1:
|
||||
if len(getattr(mod, '__path__', ())) > 1:
|
||||
# A namespace package, which confuses this code, so ignore it.
|
||||
continue
|
||||
|
||||
|
|
@ -477,10 +486,10 @@ class InOrOut:
|
|||
# of tracing anyway.
|
||||
continue
|
||||
if disp.trace:
|
||||
msg = f"Already imported a file that will be measured: {filename}"
|
||||
self.warn(msg, slug="already-imported")
|
||||
msg = f'Already imported a file that will be measured: {filename}'
|
||||
self.warn(msg, slug='already-imported')
|
||||
warned.add(filename)
|
||||
elif self.debug and self.debug.should("trace"):
|
||||
elif self.debug and self.debug.should('trace'):
|
||||
self.debug.write(
|
||||
"Didn't trace already imported file {!r}: {}".format(
|
||||
disp.original_filename, disp.reason,
|
||||
|
|
@ -500,7 +509,7 @@ class InOrOut:
|
|||
"""
|
||||
mod = sys.modules.get(pkg)
|
||||
if mod is None:
|
||||
self.warn(f"Module {pkg} was never imported.", slug="module-not-imported")
|
||||
self.warn(f'Module {pkg} was never imported.', slug='module-not-imported')
|
||||
return
|
||||
|
||||
if module_is_namespace(mod):
|
||||
|
|
@ -509,14 +518,14 @@ class InOrOut:
|
|||
return
|
||||
|
||||
if not module_has_file(mod):
|
||||
self.warn(f"Module {pkg} has no Python source.", slug="module-not-python")
|
||||
self.warn(f'Module {pkg} has no Python source.', slug='module-not-python')
|
||||
return
|
||||
|
||||
# The module was in sys.modules, and seems like a module with code, but
|
||||
# we never measured it. I guess that means it was imported before
|
||||
# coverage even started.
|
||||
msg = f"Module {pkg} was previously imported, but not measured"
|
||||
self.warn(msg, slug="module-not-measured")
|
||||
msg = f'Module {pkg} was previously imported, but not measured'
|
||||
self.warn(msg, slug='module-not-measured')
|
||||
|
||||
def find_possibly_unexecuted_files(self) -> Iterable[tuple[str, str | None]]:
|
||||
"""Find files in the areas of interest that might be untraced.
|
||||
|
|
@ -524,8 +533,10 @@ class InOrOut:
|
|||
Yields pairs: file path, and responsible plug-in name.
|
||||
"""
|
||||
for pkg in self.source_pkgs:
|
||||
if (pkg not in sys.modules or
|
||||
not module_has_file(sys.modules[pkg])):
|
||||
if (
|
||||
pkg not in sys.modules or
|
||||
not module_has_file(sys.modules[pkg])
|
||||
):
|
||||
continue
|
||||
pkg_file = source_for_file(cast(str, sys.modules[pkg].__file__))
|
||||
yield from self._find_executable_files(canonical_path(pkg_file))
|
||||
|
|
@ -569,16 +580,16 @@ class InOrOut:
|
|||
Returns a list of (key, value) pairs.
|
||||
"""
|
||||
info = [
|
||||
("coverage_paths", self.cover_paths),
|
||||
("stdlib_paths", self.pylib_paths),
|
||||
("third_party_paths", self.third_paths),
|
||||
("source_in_third_party_paths", self.source_in_third_paths),
|
||||
('coverage_paths', self.cover_paths),
|
||||
('stdlib_paths', self.pylib_paths),
|
||||
('third_party_paths', self.third_paths),
|
||||
('source_in_third_party_paths', self.source_in_third_paths),
|
||||
]
|
||||
|
||||
matcher_names = [
|
||||
"source_match", "source_pkgs_match",
|
||||
"include_match", "omit_match",
|
||||
"cover_match", "pylib_match", "third_match", "source_in_third_match",
|
||||
'source_match', 'source_pkgs_match',
|
||||
'include_match', 'omit_match',
|
||||
'cover_match', 'pylib_match', 'third_match', 'source_in_third_match',
|
||||
]
|
||||
|
||||
for matcher_name in matcher_names:
|
||||
|
|
@ -586,7 +597,7 @@ class InOrOut:
|
|||
if matcher:
|
||||
matcher_info = matcher.info()
|
||||
else:
|
||||
matcher_info = "-none-"
|
||||
matcher_info = '-none-'
|
||||
info.append((matcher_name, matcher_info))
|
||||
|
||||
return info
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Json reporting for coverage.py"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import sys
|
||||
|
||||
from typing import Any, IO, Iterable, TYPE_CHECKING
|
||||
from typing import Any
|
||||
from typing import IO
|
||||
from typing import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from coverage import __version__
|
||||
from coverage.report_core import get_analysis_to_report
|
||||
from coverage.results import Analysis, Numbers
|
||||
from coverage.types import TMorf, TLineNo
|
||||
from coverage.results import Analysis
|
||||
from coverage.results import Numbers
|
||||
from coverage.types import TLineNo
|
||||
from coverage.types import TMorf
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from coverage import Coverage
|
||||
|
|
@ -25,10 +27,11 @@ if TYPE_CHECKING:
|
|||
# 2: add the meta.format field.
|
||||
FORMAT_VERSION = 2
|
||||
|
||||
|
||||
class JsonReporter:
|
||||
"""A reporter for writing JSON coverage results."""
|
||||
|
||||
report_type = "JSON report"
|
||||
report_type = 'JSON report'
|
||||
|
||||
def __init__(self, coverage: Coverage) -> None:
|
||||
self.coverage = coverage
|
||||
|
|
@ -47,12 +50,12 @@ class JsonReporter:
|
|||
outfile = outfile or sys.stdout
|
||||
coverage_data = self.coverage.get_data()
|
||||
coverage_data.set_query_contexts(self.config.report_contexts)
|
||||
self.report_data["meta"] = {
|
||||
"format": FORMAT_VERSION,
|
||||
"version": __version__,
|
||||
"timestamp": datetime.datetime.now().isoformat(),
|
||||
"branch_coverage": coverage_data.has_arcs(),
|
||||
"show_contexts": self.config.json_show_contexts,
|
||||
self.report_data['meta'] = {
|
||||
'format': FORMAT_VERSION,
|
||||
'version': __version__,
|
||||
'timestamp': datetime.datetime.now().isoformat(),
|
||||
'branch_coverage': coverage_data.has_arcs(),
|
||||
'show_contexts': self.config.json_show_contexts,
|
||||
}
|
||||
|
||||
measured_files = {}
|
||||
|
|
@ -62,23 +65,23 @@ class JsonReporter:
|
|||
analysis,
|
||||
)
|
||||
|
||||
self.report_data["files"] = measured_files
|
||||
self.report_data['files'] = measured_files
|
||||
|
||||
self.report_data["totals"] = {
|
||||
"covered_lines": self.total.n_executed,
|
||||
"num_statements": self.total.n_statements,
|
||||
"percent_covered": self.total.pc_covered,
|
||||
"percent_covered_display": self.total.pc_covered_str,
|
||||
"missing_lines": self.total.n_missing,
|
||||
"excluded_lines": self.total.n_excluded,
|
||||
self.report_data['totals'] = {
|
||||
'covered_lines': self.total.n_executed,
|
||||
'num_statements': self.total.n_statements,
|
||||
'percent_covered': self.total.pc_covered,
|
||||
'percent_covered_display': self.total.pc_covered_str,
|
||||
'missing_lines': self.total.n_missing,
|
||||
'excluded_lines': self.total.n_excluded,
|
||||
}
|
||||
|
||||
if coverage_data.has_arcs():
|
||||
self.report_data["totals"].update({
|
||||
"num_branches": self.total.n_branches,
|
||||
"num_partial_branches": self.total.n_partial_branches,
|
||||
"covered_branches": self.total.n_executed_branches,
|
||||
"missing_branches": self.total.n_missing_branches,
|
||||
self.report_data['totals'].update({
|
||||
'num_branches': self.total.n_branches,
|
||||
'num_partial_branches': self.total.n_partial_branches,
|
||||
'covered_branches': self.total.n_executed_branches,
|
||||
'missing_branches': self.total.n_missing_branches,
|
||||
})
|
||||
|
||||
json.dump(
|
||||
|
|
@ -94,32 +97,32 @@ class JsonReporter:
|
|||
nums = analysis.numbers
|
||||
self.total += nums
|
||||
summary = {
|
||||
"covered_lines": nums.n_executed,
|
||||
"num_statements": nums.n_statements,
|
||||
"percent_covered": nums.pc_covered,
|
||||
"percent_covered_display": nums.pc_covered_str,
|
||||
"missing_lines": nums.n_missing,
|
||||
"excluded_lines": nums.n_excluded,
|
||||
'covered_lines': nums.n_executed,
|
||||
'num_statements': nums.n_statements,
|
||||
'percent_covered': nums.pc_covered,
|
||||
'percent_covered_display': nums.pc_covered_str,
|
||||
'missing_lines': nums.n_missing,
|
||||
'excluded_lines': nums.n_excluded,
|
||||
}
|
||||
reported_file = {
|
||||
"executed_lines": sorted(analysis.executed),
|
||||
"summary": summary,
|
||||
"missing_lines": sorted(analysis.missing),
|
||||
"excluded_lines": sorted(analysis.excluded),
|
||||
'executed_lines': sorted(analysis.executed),
|
||||
'summary': summary,
|
||||
'missing_lines': sorted(analysis.missing),
|
||||
'excluded_lines': sorted(analysis.excluded),
|
||||
}
|
||||
if self.config.json_show_contexts:
|
||||
reported_file["contexts"] = analysis.data.contexts_by_lineno(analysis.filename)
|
||||
reported_file['contexts'] = analysis.data.contexts_by_lineno(analysis.filename)
|
||||
if coverage_data.has_arcs():
|
||||
summary.update({
|
||||
"num_branches": nums.n_branches,
|
||||
"num_partial_branches": nums.n_partial_branches,
|
||||
"covered_branches": nums.n_executed_branches,
|
||||
"missing_branches": nums.n_missing_branches,
|
||||
'num_branches': nums.n_branches,
|
||||
'num_partial_branches': nums.n_partial_branches,
|
||||
'covered_branches': nums.n_executed_branches,
|
||||
'missing_branches': nums.n_missing_branches,
|
||||
})
|
||||
reported_file["executed_branches"] = list(
|
||||
reported_file['executed_branches'] = list(
|
||||
_convert_branch_arcs(analysis.executed_branch_arcs()),
|
||||
)
|
||||
reported_file["missing_branches"] = list(
|
||||
reported_file['missing_branches'] = list(
|
||||
_convert_branch_arcs(analysis.missing_branch_arcs()),
|
||||
)
|
||||
return reported_file
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""LCOV reporting for coverage.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import sys
|
||||
|
||||
from typing import IO, Iterable, TYPE_CHECKING
|
||||
from typing import IO
|
||||
from typing import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from coverage.plugin import FileReporter
|
||||
from coverage.report_core import get_analysis_to_report
|
||||
from coverage.results import Analysis, Numbers
|
||||
from coverage.results import Analysis
|
||||
from coverage.results import Numbers
|
||||
from coverage.types import TMorf
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -22,14 +22,14 @@ if TYPE_CHECKING:
|
|||
|
||||
def line_hash(line: str) -> str:
|
||||
"""Produce a hash of a source line for use in the LCOV file."""
|
||||
hashed = hashlib.md5(line.encode("utf-8")).digest()
|
||||
return base64.b64encode(hashed).decode("ascii").rstrip("=")
|
||||
hashed = hashlib.md5(line.encode('utf-8')).digest()
|
||||
return base64.b64encode(hashed).decode('ascii').rstrip('=')
|
||||
|
||||
|
||||
class LcovReporter:
|
||||
"""A reporter for writing LCOV coverage reports."""
|
||||
|
||||
report_type = "LCOV report"
|
||||
report_type = 'LCOV report'
|
||||
|
||||
def __init__(self, coverage: Coverage) -> None:
|
||||
self.coverage = coverage
|
||||
|
|
@ -59,8 +59,8 @@ class LcovReporter:
|
|||
"""
|
||||
self.total += analysis.numbers
|
||||
|
||||
outfile.write("TN:\n")
|
||||
outfile.write(f"SF:{fr.relative_filename()}\n")
|
||||
outfile.write('TN:\n')
|
||||
outfile.write(f'SF:{fr.relative_filename()}\n')
|
||||
source_lines = fr.source().splitlines()
|
||||
for covered in sorted(analysis.executed):
|
||||
if covered in analysis.excluded:
|
||||
|
|
@ -76,22 +76,22 @@ class LcovReporter:
|
|||
# characters of the encoding ("==") are removed from the hash to
|
||||
# allow genhtml to run on the resulting lcov file.
|
||||
if source_lines:
|
||||
if covered-1 >= len(source_lines):
|
||||
if covered - 1 >= len(source_lines):
|
||||
break
|
||||
line = source_lines[covered-1]
|
||||
line = source_lines[covered - 1]
|
||||
else:
|
||||
line = ""
|
||||
outfile.write(f"DA:{covered},1,{line_hash(line)}\n")
|
||||
line = ''
|
||||
outfile.write(f'DA:{covered},1,{line_hash(line)}\n')
|
||||
|
||||
for missed in sorted(analysis.missing):
|
||||
# We don't have to skip excluded lines here, because `missing`
|
||||
# already doesn't have them.
|
||||
assert source_lines
|
||||
line = source_lines[missed-1]
|
||||
outfile.write(f"DA:{missed},0,{line_hash(line)}\n")
|
||||
line = source_lines[missed - 1]
|
||||
outfile.write(f'DA:{missed},0,{line_hash(line)}\n')
|
||||
|
||||
outfile.write(f"LF:{analysis.numbers.n_statements}\n")
|
||||
outfile.write(f"LH:{analysis.numbers.n_executed}\n")
|
||||
outfile.write(f'LF:{analysis.numbers.n_statements}\n')
|
||||
outfile.write(f'LH:{analysis.numbers.n_executed}\n')
|
||||
|
||||
# More information dense branch coverage data.
|
||||
missing_arcs = analysis.missing_branch_arcs()
|
||||
|
|
@ -107,7 +107,7 @@ class LcovReporter:
|
|||
# the line number of the exit branch to 0 will allow
|
||||
# for valid lcov, while preserving the data.
|
||||
line_number = max(line_number, 0)
|
||||
outfile.write(f"BRDA:{line_number},{block_number},{branch_number},-\n")
|
||||
outfile.write(f'BRDA:{line_number},{block_number},{branch_number},-\n')
|
||||
|
||||
# The start value below allows for the block number to be
|
||||
# preserved between these two for loops (stopping the loop from
|
||||
|
|
@ -117,14 +117,14 @@ class LcovReporter:
|
|||
start=len(missing_arcs[block_line_number]),
|
||||
):
|
||||
line_number = max(line_number, 0)
|
||||
outfile.write(f"BRDA:{line_number},{block_number},{branch_number},1\n")
|
||||
outfile.write(f'BRDA:{line_number},{block_number},{branch_number},1\n')
|
||||
|
||||
# Summary of the branch coverage.
|
||||
if analysis.has_arcs():
|
||||
branch_stats = analysis.branch_stats()
|
||||
brf = sum(t for t, k in branch_stats.values())
|
||||
brh = brf - sum(t - k for t, k in branch_stats.values())
|
||||
outfile.write(f"BRF:{brf}\n")
|
||||
outfile.write(f"BRH:{brh}\n")
|
||||
outfile.write(f'BRF:{brf}\n')
|
||||
outfile.write(f'BRH:{brh}\n')
|
||||
|
||||
outfile.write("end_of_record\n")
|
||||
outfile.write('end_of_record\n')
|
||||
|
|
|
|||
|
|
@ -1,37 +1,37 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Miscellaneous stuff for coverage.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import datetime
|
||||
import errno
|
||||
import hashlib
|
||||
import importlib
|
||||
import importlib.util
|
||||
import inspect
|
||||
import locale
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
|
||||
from types import ModuleType
|
||||
from typing import (
|
||||
Any, Callable, IO, Iterable, Iterator, Mapping, NoReturn, Sequence, TypeVar,
|
||||
)
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import IO
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import Mapping
|
||||
from typing import NoReturn
|
||||
from typing import Sequence
|
||||
from typing import TypeVar
|
||||
|
||||
from coverage import env
|
||||
from coverage.exceptions import * # pylint: disable=wildcard-import
|
||||
from coverage.exceptions import CoverageException
|
||||
from coverage.types import TArc
|
||||
|
||||
# In 6.0, the exceptions moved from misc.py to exceptions.py. But a number of
|
||||
# other packages were importing the exceptions from misc, so import them here.
|
||||
# pylint: disable=unused-wildcard-import
|
||||
from coverage.exceptions import * # pylint: disable=wildcard-import
|
||||
|
||||
ISOLATED_MODULES: dict[ModuleType, ModuleType] = {}
|
||||
|
||||
|
|
@ -54,11 +54,13 @@ def isolate_module(mod: ModuleType) -> ModuleType:
|
|||
setattr(new_mod, name, value)
|
||||
return ISOLATED_MODULES[mod]
|
||||
|
||||
|
||||
os = isolate_module(os)
|
||||
|
||||
|
||||
class SysModuleSaver:
|
||||
"""Saves the contents of sys.modules, and removes new modules later."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.old_modules = set(sys.modules)
|
||||
|
||||
|
|
@ -111,13 +113,14 @@ def nice_pair(pair: TArc) -> str:
|
|||
"""
|
||||
start, end = pair
|
||||
if start == end:
|
||||
return "%d" % start
|
||||
return '%d' % start
|
||||
else:
|
||||
return "%d-%d" % (start, end)
|
||||
return '%d-%d' % (start, end)
|
||||
|
||||
|
||||
TSelf = TypeVar("TSelf")
|
||||
TRetVal = TypeVar("TRetVal")
|
||||
TSelf = TypeVar('TSelf')
|
||||
TRetVal = TypeVar('TRetVal')
|
||||
|
||||
|
||||
def expensive(fn: Callable[[TSelf], TRetVal]) -> Callable[[TSelf], TRetVal]:
|
||||
"""A decorator to indicate that a method shouldn't be called more than once.
|
||||
|
|
@ -127,7 +130,7 @@ def expensive(fn: Callable[[TSelf], TRetVal]) -> Callable[[TSelf], TRetVal]:
|
|||
|
||||
"""
|
||||
if env.TESTING:
|
||||
attr = "_once_" + fn.__name__
|
||||
attr = '_once_' + fn.__name__
|
||||
|
||||
def _wrapper(self: TSelf) -> TRetVal:
|
||||
if hasattr(self, attr):
|
||||
|
|
@ -153,7 +156,7 @@ def join_regex(regexes: Iterable[str]) -> str:
|
|||
if len(regexes) == 1:
|
||||
return regexes[0]
|
||||
else:
|
||||
return "|".join(f"(?:{r})" for r in regexes)
|
||||
return '|'.join(f'(?:{r})' for r in regexes)
|
||||
|
||||
|
||||
def file_be_gone(path: str) -> None:
|
||||
|
|
@ -184,8 +187,8 @@ def output_encoding(outfile: IO[str] | None = None) -> str:
|
|||
if outfile is None:
|
||||
outfile = sys.stdout
|
||||
encoding = (
|
||||
getattr(outfile, "encoding", None) or
|
||||
getattr(sys.__stdout__, "encoding", None) or
|
||||
getattr(outfile, 'encoding', None) or
|
||||
getattr(sys.__stdout__, 'encoding', None) or
|
||||
locale.getpreferredencoding()
|
||||
)
|
||||
return encoding
|
||||
|
|
@ -193,20 +196,21 @@ def output_encoding(outfile: IO[str] | None = None) -> str:
|
|||
|
||||
class Hasher:
|
||||
"""Hashes Python data for fingerprinting."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.hash = hashlib.new("sha3_256")
|
||||
self.hash = hashlib.new('sha3_256')
|
||||
|
||||
def update(self, v: Any) -> None:
|
||||
"""Add `v` to the hash, recursively if needed."""
|
||||
self.hash.update(str(type(v)).encode("utf-8"))
|
||||
self.hash.update(str(type(v)).encode('utf-8'))
|
||||
if isinstance(v, str):
|
||||
self.hash.update(v.encode("utf-8"))
|
||||
self.hash.update(v.encode('utf-8'))
|
||||
elif isinstance(v, bytes):
|
||||
self.hash.update(v)
|
||||
elif v is None:
|
||||
pass
|
||||
elif isinstance(v, (int, float)):
|
||||
self.hash.update(str(v).encode("utf-8"))
|
||||
self.hash.update(str(v).encode('utf-8'))
|
||||
elif isinstance(v, (tuple, list)):
|
||||
for e in v:
|
||||
self.update(e)
|
||||
|
|
@ -217,14 +221,14 @@ class Hasher:
|
|||
self.update(v[k])
|
||||
else:
|
||||
for k in dir(v):
|
||||
if k.startswith("__"):
|
||||
if k.startswith('__'):
|
||||
continue
|
||||
a = getattr(v, k)
|
||||
if inspect.isroutine(a):
|
||||
continue
|
||||
self.update(k)
|
||||
self.update(a)
|
||||
self.hash.update(b".")
|
||||
self.hash.update(b'.')
|
||||
|
||||
def hexdigest(self) -> str:
|
||||
"""Retrieve the hex digest of the hash."""
|
||||
|
|
@ -233,16 +237,16 @@ class Hasher:
|
|||
|
||||
def _needs_to_implement(that: Any, func_name: str) -> NoReturn:
|
||||
"""Helper to raise NotImplementedError in interface stubs."""
|
||||
if hasattr(that, "_coverage_plugin_name"):
|
||||
thing = "Plugin"
|
||||
if hasattr(that, '_coverage_plugin_name'):
|
||||
thing = 'Plugin'
|
||||
name = that._coverage_plugin_name
|
||||
else:
|
||||
thing = "Class"
|
||||
thing = 'Class'
|
||||
klass = that.__class__
|
||||
name = f"{klass.__module__}.{klass.__name__}"
|
||||
name = f'{klass.__module__}.{klass.__name__}'
|
||||
|
||||
raise NotImplementedError(
|
||||
f"{thing} {name!r} needs to implement {func_name}()",
|
||||
f'{thing} {name!r} needs to implement {func_name}()',
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -253,6 +257,7 @@ class DefaultValue:
|
|||
and Sphinx output.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, display_as: str) -> None:
|
||||
self.display_as = display_as
|
||||
|
||||
|
|
@ -291,21 +296,21 @@ def substitute_variables(text: str, variables: Mapping[str, str]) -> str:
|
|||
)
|
||||
"""
|
||||
|
||||
dollar_groups = ("dollar", "word1", "word2")
|
||||
dollar_groups = ('dollar', 'word1', 'word2')
|
||||
|
||||
def dollar_replace(match: re.Match[str]) -> str:
|
||||
"""Called for each $replacement."""
|
||||
# Only one of the dollar_groups will have matched, just get its text.
|
||||
word = next(g for g in match.group(*dollar_groups) if g) # pragma: always breaks
|
||||
if word == "$":
|
||||
return "$"
|
||||
if word == '$':
|
||||
return '$'
|
||||
elif word in variables:
|
||||
return variables[word]
|
||||
elif match["strict"]:
|
||||
msg = f"Variable {word} is undefined: {text!r}"
|
||||
elif match['strict']:
|
||||
msg = f'Variable {word} is undefined: {text!r}'
|
||||
raise CoverageException(msg)
|
||||
else:
|
||||
return match["defval"]
|
||||
return match['defval']
|
||||
|
||||
text = re.sub(dollar_pattern, dollar_replace, text)
|
||||
return text
|
||||
|
|
@ -314,7 +319,7 @@ def substitute_variables(text: str, variables: Mapping[str, str]) -> str:
|
|||
def format_local_datetime(dt: datetime.datetime) -> str:
|
||||
"""Return a string with local timezone representing the date.
|
||||
"""
|
||||
return dt.astimezone().strftime("%Y-%m-%d %H:%M %z")
|
||||
return dt.astimezone().strftime('%Y-%m-%d %H:%M %z')
|
||||
|
||||
|
||||
def import_local_file(modname: str, modfile: str | None = None) -> ModuleType:
|
||||
|
|
@ -326,7 +331,7 @@ def import_local_file(modname: str, modfile: str | None = None) -> ModuleType:
|
|||
|
||||
"""
|
||||
if modfile is None:
|
||||
modfile = modname + ".py"
|
||||
modfile = modname + '.py'
|
||||
spec = importlib.util.spec_from_file_location(modname, modfile)
|
||||
assert spec is not None
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
|
|
@ -352,7 +357,8 @@ def _human_key(s: str) -> tuple[list[str | int], str]:
|
|||
except ValueError:
|
||||
return s
|
||||
|
||||
return ([tryint(c) for c in re.split(r"(\d+)", s)], s)
|
||||
return ([tryint(c) for c in re.split(r'(\d+)', s)], s)
|
||||
|
||||
|
||||
def human_sorted(strings: Iterable[str]) -> list[str]:
|
||||
"""Sort the given iterable of strings the way that humans expect.
|
||||
|
|
@ -364,7 +370,9 @@ def human_sorted(strings: Iterable[str]) -> list[str]:
|
|||
"""
|
||||
return sorted(strings, key=_human_key)
|
||||
|
||||
SortableItem = TypeVar("SortableItem", bound=Sequence[Any])
|
||||
|
||||
SortableItem = TypeVar('SortableItem', bound=Sequence[Any])
|
||||
|
||||
|
||||
def human_sorted_items(
|
||||
items: Iterable[SortableItem],
|
||||
|
|
@ -380,7 +388,7 @@ def human_sorted_items(
|
|||
return sorted(items, key=lambda item: (_human_key(item[0]), *item[1:]), reverse=reverse)
|
||||
|
||||
|
||||
def plural(n: int, thing: str = "", things: str = "") -> str:
|
||||
def plural(n: int, thing: str = '', things: str = '') -> str:
|
||||
"""Pluralize a word.
|
||||
|
||||
If n is 1, return thing. Otherwise return things, or thing+s.
|
||||
|
|
@ -388,7 +396,7 @@ def plural(n: int, thing: str = "", things: str = "") -> str:
|
|||
if n == 1:
|
||||
return thing
|
||||
else:
|
||||
return things or (thing + "s")
|
||||
return things or (thing + 's')
|
||||
|
||||
|
||||
def stdout_link(text: str, url: str) -> str:
|
||||
|
|
@ -397,7 +405,7 @@ def stdout_link(text: str, url: str) -> str:
|
|||
If attached to a terminal, use escape sequences. Otherwise, just return
|
||||
the text.
|
||||
"""
|
||||
if hasattr(sys.stdout, "isatty") and sys.stdout.isatty():
|
||||
return f"\033]8;;{url}\a{text}\033]8;;\a"
|
||||
if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty():
|
||||
return f'\033]8;;{url}\a{text}\033]8;;\a'
|
||||
else:
|
||||
return text
|
||||
|
|
|
|||
|
|
@ -1,29 +1,25 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Monkey-patching to add multiprocessing support for coverage.py"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import multiprocessing
|
||||
import multiprocessing.process
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from typing import Any
|
||||
|
||||
from coverage.debug import DebugControl
|
||||
|
||||
# An attribute that will be set on the module to indicate that it has been
|
||||
# monkey-patched.
|
||||
PATCHED_MARKER = "_coverage$patched"
|
||||
PATCHED_MARKER = '_coverage$patched'
|
||||
|
||||
|
||||
OriginalProcess = multiprocessing.process.BaseProcess
|
||||
original_bootstrap = OriginalProcess._bootstrap # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class ProcessWithCoverage(OriginalProcess): # pylint: disable=abstract-method
|
||||
"""A replacement for multiprocess.Process that starts coverage."""
|
||||
|
||||
|
|
@ -37,12 +33,12 @@ class ProcessWithCoverage(OriginalProcess): # pylint: disable=abstract-m
|
|||
cov.start()
|
||||
_debug = cov._debug
|
||||
assert _debug is not None
|
||||
if _debug.should("multiproc"):
|
||||
if _debug.should('multiproc'):
|
||||
debug = _debug
|
||||
if debug:
|
||||
debug.write("Calling multiprocessing bootstrap")
|
||||
debug.write('Calling multiprocessing bootstrap')
|
||||
except Exception:
|
||||
print("Exception during multiprocessing bootstrap init:", file=sys.stderr)
|
||||
print('Exception during multiprocessing bootstrap init:', file=sys.stderr)
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
sys.stderr.flush()
|
||||
raise
|
||||
|
|
@ -50,27 +46,29 @@ class ProcessWithCoverage(OriginalProcess): # pylint: disable=abstract-m
|
|||
return original_bootstrap(self, *args, **kwargs)
|
||||
finally:
|
||||
if debug:
|
||||
debug.write("Finished multiprocessing bootstrap")
|
||||
debug.write('Finished multiprocessing bootstrap')
|
||||
try:
|
||||
cov.stop()
|
||||
cov.save()
|
||||
except Exception as exc:
|
||||
if debug:
|
||||
debug.write("Exception during multiprocessing bootstrap cleanup", exc=exc)
|
||||
debug.write('Exception during multiprocessing bootstrap cleanup', exc=exc)
|
||||
raise
|
||||
if debug:
|
||||
debug.write("Saved multiprocessing data")
|
||||
debug.write('Saved multiprocessing data')
|
||||
|
||||
|
||||
class Stowaway:
|
||||
"""An object to pickle, so when it is unpickled, it can apply the monkey-patch."""
|
||||
|
||||
def __init__(self, rcfile: str) -> None:
|
||||
self.rcfile = rcfile
|
||||
|
||||
def __getstate__(self) -> dict[str, str]:
|
||||
return {"rcfile": self.rcfile}
|
||||
return {'rcfile': self.rcfile}
|
||||
|
||||
def __setstate__(self, state: dict[str, str]) -> None:
|
||||
patch_multiprocessing(state["rcfile"])
|
||||
patch_multiprocessing(state['rcfile'])
|
||||
|
||||
|
||||
def patch_multiprocessing(rcfile: str) -> None:
|
||||
|
|
@ -90,7 +88,7 @@ def patch_multiprocessing(rcfile: str) -> None:
|
|||
|
||||
# Set the value in ProcessWithCoverage that will be pickled into the child
|
||||
# process.
|
||||
os.environ["COVERAGE_RCFILE"] = os.path.abspath(rcfile)
|
||||
os.environ['COVERAGE_RCFILE'] = os.path.abspath(rcfile)
|
||||
|
||||
# When spawning processes rather than forking them, we have no state in the
|
||||
# new process. We sneak in there with a Stowaway: we stuff one of our own
|
||||
|
|
@ -107,7 +105,7 @@ def patch_multiprocessing(rcfile: str) -> None:
|
|||
def get_preparation_data_with_stowaway(name: str) -> dict[str, Any]:
|
||||
"""Get the original preparation data, and also insert our stowaway."""
|
||||
d = original_get_preparation_data(name)
|
||||
d["stowaway"] = Stowaway(rcfile)
|
||||
d['stowaway'] = Stowaway(rcfile)
|
||||
return d
|
||||
|
||||
spawn.get_preparation_data = get_preparation_data_with_stowaway
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""
|
||||
Functions to manipulate packed binary representations of number sets.
|
||||
|
||||
|
|
@ -13,12 +12,10 @@ in the blobs should be considered an implementation detail that might change in
|
|||
the future. Use these functions to work with those binary blobs of data.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
|
||||
from itertools import zip_longest
|
||||
from typing import Iterable
|
||||
|
||||
|
|
@ -36,10 +33,10 @@ def nums_to_numbits(nums: Iterable[int]) -> bytes:
|
|||
nbytes = max(nums) // 8 + 1
|
||||
except ValueError:
|
||||
# nums was empty.
|
||||
return b""
|
||||
return b''
|
||||
b = bytearray(nbytes)
|
||||
for num in nums:
|
||||
b[num//8] |= 1 << num % 8
|
||||
b[num // 8] |= 1 << num % 8
|
||||
return bytes(b)
|
||||
|
||||
|
||||
|
|
@ -82,7 +79,7 @@ def numbits_intersection(numbits1: bytes, numbits2: bytes) -> bytes:
|
|||
"""
|
||||
byte_pairs = zip_longest(numbits1, numbits2, fillvalue=0)
|
||||
intersection_bytes = bytes(b1 & b2 for b1, b2 in byte_pairs)
|
||||
return intersection_bytes.rstrip(b"\0")
|
||||
return intersection_bytes.rstrip(b'\0')
|
||||
|
||||
|
||||
def numbits_any_intersection(numbits1: bytes, numbits2: bytes) -> bool:
|
||||
|
|
@ -140,8 +137,8 @@ def register_sqlite_functions(connection: sqlite3.Connection) -> None:
|
|||
(47,)
|
||||
)
|
||||
"""
|
||||
connection.create_function("numbits_union", 2, numbits_union)
|
||||
connection.create_function("numbits_intersection", 2, numbits_intersection)
|
||||
connection.create_function("numbits_any_intersection", 2, numbits_any_intersection)
|
||||
connection.create_function("num_in_numbits", 2, num_in_numbits)
|
||||
connection.create_function("numbits_to_nums", 1, lambda b: json.dumps(numbits_to_nums(b)))
|
||||
connection.create_function('numbits_union', 2, numbits_union)
|
||||
connection.create_function('numbits_intersection', 2, numbits_intersection)
|
||||
connection.create_function('numbits_any_intersection', 2, numbits_any_intersection)
|
||||
connection.create_function('num_in_numbits', 2, num_in_numbits)
|
||||
connection.create_function('numbits_to_nums', 1, lambda b: json.dumps(numbits_to_nums(b)))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Code parsing for coverage.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
|
|
@ -12,21 +10,30 @@ import re
|
|||
import sys
|
||||
import token
|
||||
import tokenize
|
||||
|
||||
from dataclasses import dataclass
|
||||
from types import CodeType
|
||||
from typing import (
|
||||
cast, Any, Callable, Dict, Iterable, List, Optional, Protocol, Sequence,
|
||||
Set, Tuple,
|
||||
)
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Protocol
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
|
||||
from coverage import env
|
||||
from coverage.bytecode import code_objects
|
||||
from coverage.debug import short_stack
|
||||
from coverage.exceptions import NoSource, NotPython
|
||||
from coverage.misc import join_regex, nice_pair
|
||||
from coverage.exceptions import NoSource
|
||||
from coverage.exceptions import NotPython
|
||||
from coverage.misc import join_regex
|
||||
from coverage.misc import nice_pair
|
||||
from coverage.phystokens import generate_tokens
|
||||
from coverage.types import TArc, TLineNo
|
||||
from coverage.types import TArc
|
||||
from coverage.types import TLineNo
|
||||
|
||||
|
||||
class PythonParser:
|
||||
|
|
@ -36,6 +43,7 @@ class PythonParser:
|
|||
involved.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text: str | None = None,
|
||||
|
|
@ -48,8 +56,8 @@ class PythonParser:
|
|||
`exclude`, a regex string.
|
||||
|
||||
"""
|
||||
assert text or filename, "PythonParser needs either text or filename"
|
||||
self.filename = filename or "<code>"
|
||||
assert text or filename, 'PythonParser needs either text or filename'
|
||||
self.filename = filename or '<code>'
|
||||
if text is not None:
|
||||
self.text: str = text
|
||||
else:
|
||||
|
|
@ -62,7 +70,7 @@ class PythonParser:
|
|||
self.exclude = exclude
|
||||
|
||||
# The text lines of the parsed code.
|
||||
self.lines: list[str] = self.text.split("\n")
|
||||
self.lines: list[str] = self.text.split('\n')
|
||||
|
||||
# The normalized line numbers of the statements in the code. Exclusions
|
||||
# are taken into account, and statements are adjusted to their first
|
||||
|
|
@ -152,25 +160,27 @@ class PythonParser:
|
|||
tokgen = generate_tokens(self.text)
|
||||
for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen:
|
||||
if self.show_tokens: # pragma: debugging
|
||||
print("%10s %5s %-20r %r" % (
|
||||
tokenize.tok_name.get(toktype, toktype),
|
||||
nice_pair((slineno, elineno)), ttext, ltext,
|
||||
))
|
||||
print(
|
||||
'%10s %5s %-20r %r' % (
|
||||
tokenize.tok_name.get(toktype, toktype),
|
||||
nice_pair((slineno, elineno)), ttext, ltext,
|
||||
),
|
||||
)
|
||||
if toktype == token.INDENT:
|
||||
indent += 1
|
||||
elif toktype == token.DEDENT:
|
||||
indent -= 1
|
||||
elif toktype == token.NAME:
|
||||
if ttext == "class":
|
||||
if ttext == 'class':
|
||||
# Class definitions look like branches in the bytecode, so
|
||||
# we need to exclude them. The simplest way is to note the
|
||||
# lines with the "class" keyword.
|
||||
self.raw_classdefs.add(slineno)
|
||||
elif toktype == token.OP:
|
||||
if ttext == ":" and nesting == 0:
|
||||
if ttext == ':' and nesting == 0:
|
||||
should_exclude = (
|
||||
self.raw_excluded.intersection(range(first_line, elineno + 1))
|
||||
or excluding_decorators
|
||||
self.raw_excluded.intersection(range(first_line, elineno + 1)) or
|
||||
excluding_decorators
|
||||
)
|
||||
if not excluding and should_exclude:
|
||||
# Start excluding a suite. We trigger off of the colon
|
||||
|
|
@ -180,28 +190,28 @@ class PythonParser:
|
|||
exclude_indent = indent
|
||||
excluding = True
|
||||
excluding_decorators = False
|
||||
elif ttext == "@" and first_on_line:
|
||||
elif ttext == '@' and first_on_line:
|
||||
# A decorator.
|
||||
if elineno in self.raw_excluded:
|
||||
excluding_decorators = True
|
||||
if excluding_decorators:
|
||||
self.raw_excluded.add(elineno)
|
||||
elif ttext in "([{":
|
||||
elif ttext in '([{':
|
||||
nesting += 1
|
||||
elif ttext in ")]}":
|
||||
elif ttext in ')]}':
|
||||
nesting -= 1
|
||||
elif toktype == token.STRING:
|
||||
if prev_toktype == token.INDENT:
|
||||
# Strings that are first on an indented line are docstrings.
|
||||
# (a trick from trace.py in the stdlib.) This works for
|
||||
# 99.9999% of cases.
|
||||
self.raw_docstrings.update(range(slineno, elineno+1))
|
||||
self.raw_docstrings.update(range(slineno, elineno + 1))
|
||||
elif toktype == token.NEWLINE:
|
||||
if first_line and elineno != first_line:
|
||||
# We're at the end of a line, and we've ended on a
|
||||
# different line than the first line of the statement,
|
||||
# so record a multi-line range.
|
||||
for l in range(first_line, elineno+1):
|
||||
for l in range(first_line, elineno + 1):
|
||||
self._multiline[l] = first_line
|
||||
first_line = 0
|
||||
first_on_line = True
|
||||
|
|
@ -267,13 +277,13 @@ class PythonParser:
|
|||
try:
|
||||
self._raw_parse()
|
||||
except (tokenize.TokenError, IndentationError, SyntaxError) as err:
|
||||
if hasattr(err, "lineno"):
|
||||
if hasattr(err, 'lineno'):
|
||||
lineno = err.lineno # IndentationError
|
||||
else:
|
||||
lineno = err.args[1][0] # TokenError
|
||||
raise NotPython(
|
||||
f"Couldn't parse '{self.filename}' as Python source: " +
|
||||
f"{err.args[0]!r} at line {lineno}",
|
||||
f'{err.args[0]!r} at line {lineno}',
|
||||
) from err
|
||||
|
||||
self.excluded = self.first_lines(self.raw_excluded)
|
||||
|
|
@ -376,13 +386,13 @@ class PythonParser:
|
|||
emsg = "didn't jump to line {lineno}"
|
||||
emsg = emsg.format(lineno=end)
|
||||
|
||||
msg = f"line {actual_start} {emsg}"
|
||||
msg = f'line {actual_start} {emsg}'
|
||||
if smsg is not None:
|
||||
msg += f", because {smsg.format(lineno=actual_start)}"
|
||||
msg += f', because {smsg.format(lineno=actual_start)}'
|
||||
|
||||
msgs.append(msg)
|
||||
|
||||
return " or ".join(msgs)
|
||||
return ' or '.join(msgs)
|
||||
|
||||
|
||||
class ByteParser:
|
||||
|
|
@ -400,7 +410,7 @@ class ByteParser:
|
|||
else:
|
||||
assert filename is not None
|
||||
try:
|
||||
self.code = compile(text, filename, "exec", dont_inherit=True)
|
||||
self.code = compile(text, filename, 'exec', dont_inherit=True)
|
||||
except SyntaxError as synerr:
|
||||
raise NotPython(
|
||||
"Couldn't parse '%s' as Python source: '%s' at line %d" % (
|
||||
|
|
@ -422,7 +432,7 @@ class ByteParser:
|
|||
Uses co_lnotab described in Python/compile.c to find the
|
||||
line numbers. Produces a sequence: l0, l1, ...
|
||||
"""
|
||||
if hasattr(self.code, "co_lines"):
|
||||
if hasattr(self.code, 'co_lines'):
|
||||
# PYVERSIONS: new in 3.10
|
||||
for _, _, line in self.code.co_lines():
|
||||
if line:
|
||||
|
|
@ -477,11 +487,12 @@ class ArcStart:
|
|||
|
||||
"""
|
||||
lineno: TLineNo
|
||||
cause: str = ""
|
||||
cause: str = ''
|
||||
|
||||
|
||||
class TAddArcFn(Protocol):
|
||||
"""The type for AstArcAnalyzer.add_arc()."""
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
start: TLineNo,
|
||||
|
|
@ -491,8 +502,10 @@ class TAddArcFn(Protocol):
|
|||
) -> None:
|
||||
...
|
||||
|
||||
|
||||
TArcFragments = Dict[TArc, List[Tuple[Optional[str], Optional[str]]]]
|
||||
|
||||
|
||||
class Block:
|
||||
"""
|
||||
Blocks need to handle various exiting statements in their own ways.
|
||||
|
|
@ -503,6 +516,7 @@ class Block:
|
|||
stack.
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
def process_break_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool:
|
||||
"""Process break exits."""
|
||||
# Because break can only appear in loops, and most subclasses
|
||||
|
|
@ -526,6 +540,7 @@ class Block:
|
|||
|
||||
class LoopBlock(Block):
|
||||
"""A block on the block stack representing a `for` or `while` loop."""
|
||||
|
||||
def __init__(self, start: TLineNo) -> None:
|
||||
# The line number where the loop starts.
|
||||
self.start = start
|
||||
|
|
@ -544,6 +559,7 @@ class LoopBlock(Block):
|
|||
|
||||
class FunctionBlock(Block):
|
||||
"""A block on the block stack representing a function definition."""
|
||||
|
||||
def __init__(self, start: TLineNo, name: str) -> None:
|
||||
# The line number where the function starts.
|
||||
self.start = start
|
||||
|
|
@ -569,6 +585,7 @@ class FunctionBlock(Block):
|
|||
|
||||
class TryBlock(Block):
|
||||
"""A block on the block stack representing a `try` block."""
|
||||
|
||||
def __init__(self, handler_start: TLineNo | None, final_start: TLineNo | None) -> None:
|
||||
# The line number of the first "except" handler, if any.
|
||||
self.handler_start = handler_start
|
||||
|
|
@ -612,6 +629,7 @@ class TryBlock(Block):
|
|||
|
||||
class WithBlock(Block):
|
||||
"""A block on the block stack representing a `with` block."""
|
||||
|
||||
def __init__(self, start: TLineNo) -> None:
|
||||
# We only ever use this block if it is needed, so that we don't have to
|
||||
# check this setting in all the methods.
|
||||
|
|
@ -659,6 +677,7 @@ class NodeList(ast.AST):
|
|||
unconditional execution of one of the clauses.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, body: Sequence[ast.AST]) -> None:
|
||||
self.body = body
|
||||
self.lineno = body[0].lineno
|
||||
|
|
@ -667,8 +686,10 @@ class NodeList(ast.AST):
|
|||
# TODO: the cause messages have too many commas.
|
||||
# TODO: Shouldn't the cause messages join with "and" instead of "or"?
|
||||
|
||||
|
||||
def _make_expression_code_method(noun: str) -> Callable[[AstArcAnalyzer, ast.AST], None]:
|
||||
"""A function to make methods for expression-based callable _code_object__ methods."""
|
||||
|
||||
def _code_object__expression_callable(self: AstArcAnalyzer, node: ast.AST) -> None:
|
||||
start = self.line_for_node(node)
|
||||
self.add_arc(-start, start, None, f"didn't run the {noun} on line {start}")
|
||||
|
|
@ -692,15 +713,15 @@ class AstArcAnalyzer:
|
|||
|
||||
# Turn on AST dumps with an environment variable.
|
||||
# $set_env.py: COVERAGE_AST_DUMP - Dump the AST nodes when parsing code.
|
||||
dump_ast = bool(int(os.getenv("COVERAGE_AST_DUMP", "0")))
|
||||
dump_ast = bool(int(os.getenv('COVERAGE_AST_DUMP', '0')))
|
||||
|
||||
if dump_ast: # pragma: debugging
|
||||
# Dump the AST so that failing tests have helpful output.
|
||||
print(f"Statements: {self.statements}")
|
||||
print(f"Multiline map: {self.multiline}")
|
||||
print(f'Statements: {self.statements}')
|
||||
print(f'Multiline map: {self.multiline}')
|
||||
dumpkw: dict[str, Any] = {}
|
||||
if sys.version_info >= (3, 9):
|
||||
dumpkw["indent"] = 4
|
||||
dumpkw['indent'] = 4
|
||||
print(ast.dump(self.root_node, include_attributes=True, **dumpkw))
|
||||
|
||||
self.arcs: set[TArc] = set()
|
||||
|
|
@ -714,7 +735,7 @@ class AstArcAnalyzer:
|
|||
self.block_stack: list[Block] = []
|
||||
|
||||
# $set_env.py: COVERAGE_TRACK_ARCS - Trace possible arcs added while parsing code.
|
||||
self.debug = bool(int(os.getenv("COVERAGE_TRACK_ARCS", "0")))
|
||||
self.debug = bool(int(os.getenv('COVERAGE_TRACK_ARCS', '0')))
|
||||
|
||||
def analyze(self) -> None:
|
||||
"""Examine the AST tree from `root_node` to determine possible arcs.
|
||||
|
|
@ -725,7 +746,7 @@ class AstArcAnalyzer:
|
|||
"""
|
||||
for node in ast.walk(self.root_node):
|
||||
node_name = node.__class__.__name__
|
||||
code_object_handler = getattr(self, "_code_object__" + node_name, None)
|
||||
code_object_handler = getattr(self, '_code_object__' + node_name, None)
|
||||
if code_object_handler is not None:
|
||||
code_object_handler(node)
|
||||
|
||||
|
|
@ -738,7 +759,7 @@ class AstArcAnalyzer:
|
|||
) -> None:
|
||||
"""Add an arc, including message fragments to use if it is missing."""
|
||||
if self.debug: # pragma: debugging
|
||||
print(f"\nAdding possible arc: ({start}, {end}): {smsg!r}, {emsg!r}")
|
||||
print(f'\nAdding possible arc: ({start}, {end}): {smsg!r}, {emsg!r}')
|
||||
print(short_stack())
|
||||
self.arcs.add((start, end))
|
||||
|
||||
|
|
@ -758,7 +779,7 @@ class AstArcAnalyzer:
|
|||
node_name = node.__class__.__name__
|
||||
handler = cast(
|
||||
Optional[Callable[[ast.AST], TLineNo]],
|
||||
getattr(self, "_line__" + node_name, None),
|
||||
getattr(self, '_line__' + node_name, None),
|
||||
)
|
||||
if handler is not None:
|
||||
return handler(node)
|
||||
|
|
@ -809,8 +830,8 @@ class AstArcAnalyzer:
|
|||
|
||||
# The node types that just flow to the next node with no complications.
|
||||
OK_TO_DEFAULT = {
|
||||
"AnnAssign", "Assign", "Assert", "AugAssign", "Delete", "Expr", "Global",
|
||||
"Import", "ImportFrom", "Nonlocal", "Pass",
|
||||
'AnnAssign', 'Assign', 'Assert', 'AugAssign', 'Delete', 'Expr', 'Global',
|
||||
'Import', 'ImportFrom', 'Nonlocal', 'Pass',
|
||||
}
|
||||
|
||||
def add_arcs(self, node: ast.AST) -> set[ArcStart]:
|
||||
|
|
@ -832,7 +853,7 @@ class AstArcAnalyzer:
|
|||
node_name = node.__class__.__name__
|
||||
handler = cast(
|
||||
Optional[Callable[[ast.AST], Set[ArcStart]]],
|
||||
getattr(self, "_handle__" + node_name, None),
|
||||
getattr(self, '_handle__' + node_name, None),
|
||||
)
|
||||
if handler is not None:
|
||||
return handler(node)
|
||||
|
|
@ -841,7 +862,7 @@ class AstArcAnalyzer:
|
|||
# statement), or it's something we overlooked.
|
||||
if env.TESTING:
|
||||
if node_name not in self.OK_TO_DEFAULT:
|
||||
raise RuntimeError(f"*** Unhandled: {node}") # pragma: only failure
|
||||
raise RuntimeError(f'*** Unhandled: {node}') # pragma: only failure
|
||||
|
||||
# Default for simple statements: one exit from this node.
|
||||
return {ArcStart(self.line_for_node(node))}
|
||||
|
|
@ -898,7 +919,7 @@ class AstArcAnalyzer:
|
|||
|
||||
missing_fn = cast(
|
||||
Optional[Callable[[ast.AST], Optional[ast.AST]]],
|
||||
getattr(self, "_missing__" + node.__class__.__name__, None),
|
||||
getattr(self, '_missing__' + node.__class__.__name__, None),
|
||||
)
|
||||
if missing_fn is not None:
|
||||
ret_node = missing_fn(node)
|
||||
|
|
@ -949,8 +970,8 @@ class AstArcAnalyzer:
|
|||
new_while.lineno = body_nodes.lineno
|
||||
new_while.test = ast.Name()
|
||||
new_while.test.lineno = body_nodes.lineno
|
||||
new_while.test.id = "True"
|
||||
assert hasattr(body_nodes, "body")
|
||||
new_while.test.id = 'True'
|
||||
assert hasattr(body_nodes, 'body')
|
||||
new_while.body = body_nodes.body
|
||||
new_while.orelse = []
|
||||
return new_while
|
||||
|
|
@ -958,11 +979,11 @@ class AstArcAnalyzer:
|
|||
def is_constant_expr(self, node: ast.AST) -> str | None:
|
||||
"""Is this a compile-time constant?"""
|
||||
node_name = node.__class__.__name__
|
||||
if node_name in ["Constant", "NameConstant", "Num"]:
|
||||
return "Num"
|
||||
if node_name in ['Constant', 'NameConstant', 'Num']:
|
||||
return 'Num'
|
||||
elif isinstance(node, ast.Name):
|
||||
if node.id in ["True", "False", "None", "__debug__"]:
|
||||
return "Name"
|
||||
if node.id in ['True', 'False', 'None', '__debug__']:
|
||||
return 'Name'
|
||||
return None
|
||||
|
||||
# In the fullness of time, these might be good tests to write:
|
||||
|
|
@ -1063,7 +1084,7 @@ class AstArcAnalyzer:
|
|||
def _handle__For(self, node: ast.For) -> set[ArcStart]:
|
||||
start = self.line_for_node(node.iter)
|
||||
self.block_stack.append(LoopBlock(start=start))
|
||||
from_start = ArcStart(start, cause="the loop on line {lineno} never started")
|
||||
from_start = ArcStart(start, cause='the loop on line {lineno} never started')
|
||||
exits = self.add_body_arcs(node.body, from_start=from_start)
|
||||
# Any exit from the body will go back to the top of the loop.
|
||||
for xit in exits:
|
||||
|
|
@ -1087,9 +1108,9 @@ class AstArcAnalyzer:
|
|||
|
||||
def _handle__If(self, node: ast.If) -> set[ArcStart]:
|
||||
start = self.line_for_node(node.test)
|
||||
from_start = ArcStart(start, cause="the condition on line {lineno} was never true")
|
||||
from_start = ArcStart(start, cause='the condition on line {lineno} was never true')
|
||||
exits = self.add_body_arcs(node.body, from_start=from_start)
|
||||
from_start = ArcStart(start, cause="the condition on line {lineno} was never false")
|
||||
from_start = ArcStart(start, cause='the condition on line {lineno} was never false')
|
||||
exits |= self.add_body_arcs(node.orelse, from_start=from_start)
|
||||
return exits
|
||||
|
||||
|
|
@ -1106,16 +1127,16 @@ class AstArcAnalyzer:
|
|||
pattern = pattern.patterns[-1]
|
||||
if isinstance(pattern, ast.MatchAs):
|
||||
had_wildcard = True
|
||||
self.add_arc(last_start, case_start, "the pattern on line {lineno} always matched")
|
||||
self.add_arc(last_start, case_start, 'the pattern on line {lineno} always matched')
|
||||
from_start = ArcStart(
|
||||
case_start,
|
||||
cause="the pattern on line {lineno} never matched",
|
||||
cause='the pattern on line {lineno} never matched',
|
||||
)
|
||||
exits |= self.add_body_arcs(case.body, from_start=from_start)
|
||||
last_start = case_start
|
||||
if not had_wildcard:
|
||||
exits.add(
|
||||
ArcStart(case_start, cause="the pattern on line {lineno} always matched"),
|
||||
ArcStart(case_start, cause='the pattern on line {lineno} always matched'),
|
||||
)
|
||||
return exits
|
||||
|
||||
|
|
@ -1260,7 +1281,7 @@ class AstArcAnalyzer:
|
|||
for start in sorted(starts):
|
||||
if start.cause:
|
||||
causes.append(start.cause.format(lineno=start.lineno))
|
||||
cause = " or ".join(causes)
|
||||
cause = ' or '.join(causes)
|
||||
exits = {ArcStart(xit.lineno, cause) for xit in exits}
|
||||
return exits
|
||||
|
||||
|
|
@ -1275,7 +1296,7 @@ class AstArcAnalyzer:
|
|||
if top_is_body0:
|
||||
to_top = self.line_for_node(node.body[0])
|
||||
self.block_stack.append(LoopBlock(start=to_top))
|
||||
from_start = ArcStart(start, cause="the condition on line {lineno} was never true")
|
||||
from_start = ArcStart(start, cause='the condition on line {lineno} was never true')
|
||||
exits = self.add_body_arcs(node.body, from_start=from_start)
|
||||
for xit in exits:
|
||||
self.add_arc(xit.lineno, to_top, xit.cause)
|
||||
|
|
@ -1283,7 +1304,7 @@ class AstArcAnalyzer:
|
|||
my_block = self.block_stack.pop()
|
||||
assert isinstance(my_block, LoopBlock)
|
||||
exits.update(my_block.break_exits)
|
||||
from_start = ArcStart(start, cause="the condition on line {lineno} was never false")
|
||||
from_start = ArcStart(start, cause='the condition on line {lineno} was never false')
|
||||
if node.orelse:
|
||||
else_exits = self.add_body_arcs(node.orelse, from_start=from_start)
|
||||
exits |= else_exits
|
||||
|
|
@ -1357,9 +1378,9 @@ class AstArcAnalyzer:
|
|||
f"didn't exit the body of class {node.name!r}",
|
||||
)
|
||||
|
||||
_code_object__Lambda = _make_expression_code_method("lambda")
|
||||
_code_object__GeneratorExp = _make_expression_code_method("generator expression")
|
||||
_code_object__Lambda = _make_expression_code_method('lambda')
|
||||
_code_object__GeneratorExp = _make_expression_code_method('generator expression')
|
||||
if env.PYBEHAVIOR.comprehensions_are_functions:
|
||||
_code_object__DictComp = _make_expression_code_method("dictionary comprehension")
|
||||
_code_object__SetComp = _make_expression_code_method("set comprehension")
|
||||
_code_object__ListComp = _make_expression_code_method("list comprehension")
|
||||
_code_object__DictComp = _make_expression_code_method('dictionary comprehension')
|
||||
_code_object__SetComp = _make_expression_code_method('set comprehension')
|
||||
_code_object__ListComp = _make_expression_code_method('list comprehension')
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Better tokenizing for coverage.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
|
|
@ -12,11 +10,11 @@ import re
|
|||
import sys
|
||||
import token
|
||||
import tokenize
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
from coverage import env
|
||||
from coverage.types import TLineNo, TSourceTokenLines
|
||||
from coverage.types import TLineNo
|
||||
from coverage.types import TSourceTokenLines
|
||||
|
||||
|
||||
TokenInfos = Iterable[tokenize.TokenInfo]
|
||||
|
|
@ -34,10 +32,10 @@ def _phys_tokens(toks: TokenInfos) -> TokenInfos:
|
|||
"""
|
||||
last_line: str | None = None
|
||||
last_lineno = -1
|
||||
last_ttext: str = ""
|
||||
last_ttext: str = ''
|
||||
for ttype, ttext, (slineno, scol), (elineno, ecol), ltext in toks:
|
||||
if last_lineno != elineno:
|
||||
if last_line and last_line.endswith("\\\n"):
|
||||
if last_line and last_line.endswith('\\\n'):
|
||||
# We are at the beginning of a new line, and the last line
|
||||
# ended with a backslash. We probably have to inject a
|
||||
# backslash token into the stream. Unfortunately, there's more
|
||||
|
|
@ -54,20 +52,20 @@ def _phys_tokens(toks: TokenInfos) -> TokenInfos:
|
|||
# so we need to figure out if the backslash is already in the
|
||||
# string token or not.
|
||||
inject_backslash = True
|
||||
if last_ttext.endswith("\\"):
|
||||
if last_ttext.endswith('\\'):
|
||||
inject_backslash = False
|
||||
elif ttype == token.STRING:
|
||||
if "\n" in ttext and ttext.split("\n", 1)[0][-1] == "\\":
|
||||
if '\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
|
||||
if inject_backslash:
|
||||
# Figure out what column the backslash is in.
|
||||
ccol = len(last_line.split("\n")[-2]) - 1
|
||||
ccol = len(last_line.split('\n')[-2]) - 1
|
||||
# Yield the token, with a fake token type.
|
||||
yield tokenize.TokenInfo(
|
||||
99999, "\\\n",
|
||||
(slineno, ccol), (slineno, ccol+2),
|
||||
99999, '\\\n',
|
||||
(slineno, ccol), (slineno, ccol + 2),
|
||||
last_line,
|
||||
)
|
||||
last_line = ltext
|
||||
|
|
@ -79,6 +77,7 @@ def _phys_tokens(toks: TokenInfos) -> TokenInfos:
|
|||
|
||||
class SoftKeywordFinder(ast.NodeVisitor):
|
||||
"""Helper for finding lines with soft keywords, like match/case lines."""
|
||||
|
||||
def __init__(self, source: str) -> None:
|
||||
# This will be the set of line numbers that start with a soft keyword.
|
||||
self.soft_key_lines: set[TLineNo] = set()
|
||||
|
|
@ -119,7 +118,7 @@ def source_token_lines(source: str) -> TSourceTokenLines:
|
|||
line: list[tuple[str, str]] = []
|
||||
col = 0
|
||||
|
||||
source = source.expandtabs(8).replace("\r\n", "\n")
|
||||
source = source.expandtabs(8).replace('\r\n', '\n')
|
||||
tokgen = generate_tokens(source)
|
||||
|
||||
if env.PYBEHAVIOR.soft_keywords:
|
||||
|
|
@ -127,25 +126,25 @@ def source_token_lines(source: str) -> TSourceTokenLines:
|
|||
|
||||
for ttype, ttext, (sline, scol), (_, ecol), _ in _phys_tokens(tokgen):
|
||||
mark_start = True
|
||||
for part in re.split("(\n)", ttext):
|
||||
if part == "\n":
|
||||
for part in re.split('(\n)', ttext):
|
||||
if part == '\n':
|
||||
yield line
|
||||
line = []
|
||||
col = 0
|
||||
mark_end = False
|
||||
elif part == "":
|
||||
elif part == '':
|
||||
mark_end = False
|
||||
elif ttype in ws_tokens:
|
||||
mark_end = False
|
||||
else:
|
||||
if mark_start and scol > col:
|
||||
line.append(("ws", " " * (scol - col)))
|
||||
line.append(('ws', ' ' * (scol - col)))
|
||||
mark_start = False
|
||||
tok_class = tokenize.tok_name.get(ttype, "xx").lower()[:3]
|
||||
tok_class = tokenize.tok_name.get(ttype, 'xx').lower()[:3]
|
||||
if ttype == token.NAME:
|
||||
if keyword.iskeyword(ttext):
|
||||
# Hard keywords are always keywords.
|
||||
tok_class = "key"
|
||||
tok_class = 'key'
|
||||
elif sys.version_info >= (3, 10): # PYVERSIONS
|
||||
# Need the version_info check to keep mypy from borking
|
||||
# on issoftkeyword here.
|
||||
|
|
@ -154,12 +153,12 @@ def source_token_lines(source: str) -> TSourceTokenLines:
|
|||
# on lines that start match or case statements.
|
||||
if len(line) == 0:
|
||||
is_start_of_line = True
|
||||
elif (len(line) == 1) and line[0][0] == "ws":
|
||||
elif (len(line) == 1) and line[0][0] == 'ws':
|
||||
is_start_of_line = True
|
||||
else:
|
||||
is_start_of_line = False
|
||||
if is_start_of_line and sline in soft_key_lines:
|
||||
tok_class = "key"
|
||||
tok_class = 'key'
|
||||
line.append((tok_class, part))
|
||||
mark_end = True
|
||||
scol = 0
|
||||
|
|
@ -181,6 +180,7 @@ class CachedTokenizer:
|
|||
actually tokenize twice.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.last_text: str | None = None
|
||||
self.last_tokens: list[tokenize.TokenInfo] = []
|
||||
|
|
@ -197,6 +197,7 @@ class CachedTokenizer:
|
|||
raise
|
||||
return self.last_tokens
|
||||
|
||||
|
||||
# Create our generate_tokens cache as a callable replacement function.
|
||||
generate_tokens = CachedTokenizer().generate_tokens
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""
|
||||
.. versionadded:: 4.0
|
||||
|
||||
|
|
@ -111,17 +110,19 @@ In your ``coverage_init`` function, use the ``add_dynamic_context`` method to
|
|||
register your dynamic context switcher.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
|
||||
from types import FrameType
|
||||
from typing import Any, Iterable
|
||||
from typing import Any
|
||||
from typing import Iterable
|
||||
|
||||
from coverage import files
|
||||
from coverage.misc import _needs_to_implement
|
||||
from coverage.types import TArc, TConfigurable, TLineNo, TSourceTokenLines
|
||||
from coverage.types import TArc
|
||||
from coverage.types import TConfigurable
|
||||
from coverage.types import TLineNo
|
||||
from coverage.types import TSourceTokenLines
|
||||
|
||||
|
||||
class CoveragePlugin:
|
||||
|
|
@ -130,7 +131,7 @@ class CoveragePlugin:
|
|||
_coverage_plugin_name: str
|
||||
_coverage_enabled: bool
|
||||
|
||||
def file_tracer(self, filename: str) -> FileTracer | None: # pylint: disable=unused-argument
|
||||
def file_tracer(self, filename: str) -> FileTracer | None: # pylint: disable=unused-argument
|
||||
"""Get a :class:`FileTracer` object for a file.
|
||||
|
||||
Plug-in type: file tracer.
|
||||
|
|
@ -185,7 +186,7 @@ class CoveragePlugin:
|
|||
or the string `"python"` to have coverage.py treat the file as Python.
|
||||
|
||||
"""
|
||||
_needs_to_implement(self, "file_reporter")
|
||||
_needs_to_implement(self, 'file_reporter')
|
||||
|
||||
def dynamic_context(
|
||||
self,
|
||||
|
|
@ -287,7 +288,7 @@ class FileTracer(CoveragePluginBase):
|
|||
Returns the file name to credit with this execution.
|
||||
|
||||
"""
|
||||
_needs_to_implement(self, "source_filename")
|
||||
_needs_to_implement(self, 'source_filename')
|
||||
|
||||
def has_dynamic_source_filename(self) -> bool:
|
||||
"""Does this FileTracer have dynamic source file names?
|
||||
|
|
@ -369,7 +370,7 @@ class FileReporter(CoveragePluginBase):
|
|||
self.filename = filename
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__} filename={self.filename!r}>"
|
||||
return f'<{self.__class__.__name__} filename={self.filename!r}>'
|
||||
|
||||
def relative_filename(self) -> str:
|
||||
"""Get the relative file name for this file.
|
||||
|
|
@ -392,7 +393,7 @@ class FileReporter(CoveragePluginBase):
|
|||
as a text file, or if you need other encoding support.
|
||||
|
||||
"""
|
||||
with open(self.filename, encoding="utf-8") as f:
|
||||
with open(self.filename, encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
def lines(self) -> set[TLineNo]:
|
||||
|
|
@ -404,7 +405,7 @@ class FileReporter(CoveragePluginBase):
|
|||
Returns a set of line numbers.
|
||||
|
||||
"""
|
||||
_needs_to_implement(self, "lines")
|
||||
_needs_to_implement(self, 'lines')
|
||||
|
||||
def excluded_lines(self) -> set[TLineNo]:
|
||||
"""Get the excluded executable lines in this file.
|
||||
|
|
@ -541,7 +542,7 @@ class FileReporter(CoveragePluginBase):
|
|||
|
||||
"""
|
||||
for line in self.source().splitlines():
|
||||
yield [("txt", line)]
|
||||
yield [('txt', line)]
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, FileReporter) and self.filename == other.filename
|
||||
|
|
|
|||
|
|
@ -1,23 +1,26 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Support for plugins."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
from types import FrameType
|
||||
from typing import Any, Iterable, Iterator
|
||||
from typing import Any
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
|
||||
from coverage.exceptions import PluginError
|
||||
from coverage.misc import isolate_module
|
||||
from coverage.plugin import CoveragePlugin, FileTracer, FileReporter
|
||||
from coverage.types import (
|
||||
TArc, TConfigurable, TDebugCtl, TLineNo, TPluginConfig, TSourceTokenLines,
|
||||
)
|
||||
from coverage.plugin import CoveragePlugin
|
||||
from coverage.plugin import FileReporter
|
||||
from coverage.plugin import FileTracer
|
||||
from coverage.types import TArc
|
||||
from coverage.types import TConfigurable
|
||||
from coverage.types import TDebugCtl
|
||||
from coverage.types import TLineNo
|
||||
from coverage.types import TPluginConfig
|
||||
from coverage.types import TSourceTokenLines
|
||||
|
||||
os = isolate_module(os)
|
||||
|
||||
|
|
@ -55,7 +58,7 @@ class Plugins:
|
|||
__import__(module)
|
||||
mod = sys.modules[module]
|
||||
|
||||
coverage_init = getattr(mod, "coverage_init", None)
|
||||
coverage_init = getattr(mod, 'coverage_init', None)
|
||||
if not coverage_init:
|
||||
raise PluginError(
|
||||
f"Plugin module {module!r} didn't define a coverage_init function",
|
||||
|
|
@ -113,10 +116,10 @@ class Plugins:
|
|||
is a list to append the plugin to.
|
||||
|
||||
"""
|
||||
plugin_name = f"{self.current_module}.{plugin.__class__.__name__}"
|
||||
if self.debug and self.debug.should("plugin"):
|
||||
self.debug.write(f"Loaded plugin {self.current_module!r}: {plugin!r}")
|
||||
labelled = LabelledDebug(f"plugin {self.current_module!r}", self.debug)
|
||||
plugin_name = f'{self.current_module}.{plugin.__class__.__name__}'
|
||||
if self.debug and self.debug.should('plugin'):
|
||||
self.debug.write(f'Loaded plugin {self.current_module!r}: {plugin!r}')
|
||||
labelled = LabelledDebug(f'plugin {self.current_module!r}', self.debug)
|
||||
plugin = DebugPluginWrapper(plugin, labelled)
|
||||
|
||||
plugin._coverage_plugin_name = plugin_name
|
||||
|
|
@ -150,12 +153,12 @@ class LabelledDebug:
|
|||
|
||||
def message_prefix(self) -> str:
|
||||
"""The prefix to use on messages, combining the labels."""
|
||||
prefixes = self.labels + [""]
|
||||
return ":\n".join(" "*i+label for i, label in enumerate(prefixes))
|
||||
prefixes = self.labels + ['']
|
||||
return ':\n'.join(' ' * i + label for i, label in enumerate(prefixes))
|
||||
|
||||
def write(self, message: str) -> None:
|
||||
"""Write `message`, but with the labels prepended."""
|
||||
self.debug.write(f"{self.message_prefix()}{message}")
|
||||
self.debug.write(f'{self.message_prefix()}{message}')
|
||||
|
||||
|
||||
class DebugPluginWrapper(CoveragePlugin):
|
||||
|
|
@ -168,33 +171,33 @@ class DebugPluginWrapper(CoveragePlugin):
|
|||
|
||||
def file_tracer(self, filename: str) -> FileTracer | None:
|
||||
tracer = self.plugin.file_tracer(filename)
|
||||
self.debug.write(f"file_tracer({filename!r}) --> {tracer!r}")
|
||||
self.debug.write(f'file_tracer({filename!r}) --> {tracer!r}')
|
||||
if tracer:
|
||||
debug = self.debug.add_label(f"file {filename!r}")
|
||||
debug = self.debug.add_label(f'file {filename!r}')
|
||||
tracer = DebugFileTracerWrapper(tracer, debug)
|
||||
return tracer
|
||||
|
||||
def file_reporter(self, filename: str) -> FileReporter | str:
|
||||
reporter = self.plugin.file_reporter(filename)
|
||||
assert isinstance(reporter, FileReporter)
|
||||
self.debug.write(f"file_reporter({filename!r}) --> {reporter!r}")
|
||||
self.debug.write(f'file_reporter({filename!r}) --> {reporter!r}')
|
||||
if reporter:
|
||||
debug = self.debug.add_label(f"file {filename!r}")
|
||||
debug = self.debug.add_label(f'file {filename!r}')
|
||||
reporter = DebugFileReporterWrapper(filename, reporter, debug)
|
||||
return reporter
|
||||
|
||||
def dynamic_context(self, frame: FrameType) -> str | None:
|
||||
context = self.plugin.dynamic_context(frame)
|
||||
self.debug.write(f"dynamic_context({frame!r}) --> {context!r}")
|
||||
self.debug.write(f'dynamic_context({frame!r}) --> {context!r}')
|
||||
return context
|
||||
|
||||
def find_executable_files(self, src_dir: str) -> Iterable[str]:
|
||||
executable_files = self.plugin.find_executable_files(src_dir)
|
||||
self.debug.write(f"find_executable_files({src_dir!r}) --> {executable_files!r}")
|
||||
self.debug.write(f'find_executable_files({src_dir!r}) --> {executable_files!r}')
|
||||
return executable_files
|
||||
|
||||
def configure(self, config: TConfigurable) -> None:
|
||||
self.debug.write(f"configure({config!r})")
|
||||
self.debug.write(f'configure({config!r})')
|
||||
self.plugin.configure(config)
|
||||
|
||||
def sys_info(self) -> Iterable[tuple[str, Any]]:
|
||||
|
|
@ -210,31 +213,33 @@ class DebugFileTracerWrapper(FileTracer):
|
|||
|
||||
def _show_frame(self, frame: FrameType) -> str:
|
||||
"""A short string identifying a frame, for debug messages."""
|
||||
return "%s@%d" % (
|
||||
return '%s@%d' % (
|
||||
os.path.basename(frame.f_code.co_filename),
|
||||
frame.f_lineno,
|
||||
)
|
||||
|
||||
def source_filename(self) -> str:
|
||||
sfilename = self.tracer.source_filename()
|
||||
self.debug.write(f"source_filename() --> {sfilename!r}")
|
||||
self.debug.write(f'source_filename() --> {sfilename!r}')
|
||||
return sfilename
|
||||
|
||||
def has_dynamic_source_filename(self) -> bool:
|
||||
has = self.tracer.has_dynamic_source_filename()
|
||||
self.debug.write(f"has_dynamic_source_filename() --> {has!r}")
|
||||
self.debug.write(f'has_dynamic_source_filename() --> {has!r}')
|
||||
return has
|
||||
|
||||
def dynamic_source_filename(self, filename: str, frame: FrameType) -> str | None:
|
||||
dyn = self.tracer.dynamic_source_filename(filename, frame)
|
||||
self.debug.write("dynamic_source_filename({!r}, {}) --> {!r}".format(
|
||||
filename, self._show_frame(frame), dyn,
|
||||
))
|
||||
self.debug.write(
|
||||
'dynamic_source_filename({!r}, {}) --> {!r}'.format(
|
||||
filename, self._show_frame(frame), dyn,
|
||||
),
|
||||
)
|
||||
return dyn
|
||||
|
||||
def line_number_range(self, frame: FrameType) -> tuple[TLineNo, TLineNo]:
|
||||
pair = self.tracer.line_number_range(frame)
|
||||
self.debug.write(f"line_number_range({self._show_frame(frame)}) --> {pair!r}")
|
||||
self.debug.write(f'line_number_range({self._show_frame(frame)}) --> {pair!r}')
|
||||
return pair
|
||||
|
||||
|
||||
|
|
@ -248,50 +253,50 @@ class DebugFileReporterWrapper(FileReporter):
|
|||
|
||||
def relative_filename(self) -> str:
|
||||
ret = self.reporter.relative_filename()
|
||||
self.debug.write(f"relative_filename() --> {ret!r}")
|
||||
self.debug.write(f'relative_filename() --> {ret!r}')
|
||||
return ret
|
||||
|
||||
def lines(self) -> set[TLineNo]:
|
||||
ret = self.reporter.lines()
|
||||
self.debug.write(f"lines() --> {ret!r}")
|
||||
self.debug.write(f'lines() --> {ret!r}')
|
||||
return ret
|
||||
|
||||
def excluded_lines(self) -> set[TLineNo]:
|
||||
ret = self.reporter.excluded_lines()
|
||||
self.debug.write(f"excluded_lines() --> {ret!r}")
|
||||
self.debug.write(f'excluded_lines() --> {ret!r}')
|
||||
return ret
|
||||
|
||||
def translate_lines(self, lines: Iterable[TLineNo]) -> set[TLineNo]:
|
||||
ret = self.reporter.translate_lines(lines)
|
||||
self.debug.write(f"translate_lines({lines!r}) --> {ret!r}")
|
||||
self.debug.write(f'translate_lines({lines!r}) --> {ret!r}')
|
||||
return ret
|
||||
|
||||
def translate_arcs(self, arcs: Iterable[TArc]) -> set[TArc]:
|
||||
ret = self.reporter.translate_arcs(arcs)
|
||||
self.debug.write(f"translate_arcs({arcs!r}) --> {ret!r}")
|
||||
self.debug.write(f'translate_arcs({arcs!r}) --> {ret!r}')
|
||||
return ret
|
||||
|
||||
def no_branch_lines(self) -> set[TLineNo]:
|
||||
ret = self.reporter.no_branch_lines()
|
||||
self.debug.write(f"no_branch_lines() --> {ret!r}")
|
||||
self.debug.write(f'no_branch_lines() --> {ret!r}')
|
||||
return ret
|
||||
|
||||
def exit_counts(self) -> dict[TLineNo, int]:
|
||||
ret = self.reporter.exit_counts()
|
||||
self.debug.write(f"exit_counts() --> {ret!r}")
|
||||
self.debug.write(f'exit_counts() --> {ret!r}')
|
||||
return ret
|
||||
|
||||
def arcs(self) -> set[TArc]:
|
||||
ret = self.reporter.arcs()
|
||||
self.debug.write(f"arcs() --> {ret!r}")
|
||||
self.debug.write(f'arcs() --> {ret!r}')
|
||||
return ret
|
||||
|
||||
def source(self) -> str:
|
||||
ret = self.reporter.source()
|
||||
self.debug.write("source() --> %d chars" % (len(ret),))
|
||||
self.debug.write('source() --> %d chars' % (len(ret),))
|
||||
return ret
|
||||
|
||||
def source_token_lines(self) -> TSourceTokenLines:
|
||||
ret = list(self.reporter.source_token_lines())
|
||||
self.debug.write("source_token_lines() --> %d tokens" % (len(ret),))
|
||||
self.debug.write('source_token_lines() --> %d tokens' % (len(ret),))
|
||||
return ret
|
||||
|
|
|
|||
|
|
@ -1,24 +1,31 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Python source expertise for coverage.py"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os.path
|
||||
import types
|
||||
import zipimport
|
||||
|
||||
from typing import Iterable, TYPE_CHECKING
|
||||
from typing import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from coverage import env
|
||||
from coverage.exceptions import CoverageException, NoSource
|
||||
from coverage.files import canonical_filename, relative_filename, zip_location
|
||||
from coverage.misc import expensive, isolate_module, join_regex
|
||||
from coverage.exceptions import CoverageException
|
||||
from coverage.exceptions import NoSource
|
||||
from coverage.files import canonical_filename
|
||||
from coverage.files import relative_filename
|
||||
from coverage.files import zip_location
|
||||
from coverage.misc import expensive
|
||||
from coverage.misc import isolate_module
|
||||
from coverage.misc import join_regex
|
||||
from coverage.parser import PythonParser
|
||||
from coverage.phystokens import source_token_lines, source_encoding
|
||||
from coverage.phystokens import source_encoding
|
||||
from coverage.phystokens import source_token_lines
|
||||
from coverage.plugin import FileReporter
|
||||
from coverage.types import TArc, TLineNo, TMorf, TSourceTokenLines
|
||||
from coverage.types import TArc
|
||||
from coverage.types import TLineNo
|
||||
from coverage.types import TMorf
|
||||
from coverage.types import TSourceTokenLines
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from coverage import Coverage
|
||||
|
|
@ -32,17 +39,17 @@ def read_python_source(filename: str) -> bytes:
|
|||
Returns bytes.
|
||||
|
||||
"""
|
||||
with open(filename, "rb") as f:
|
||||
with open(filename, 'rb') as f:
|
||||
source = f.read()
|
||||
|
||||
return source.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
|
||||
return source.replace(b'\r\n', b'\n').replace(b'\r', b'\n')
|
||||
|
||||
|
||||
def get_python_source(filename: str) -> str:
|
||||
"""Return the source code, as unicode."""
|
||||
base, ext = os.path.splitext(filename)
|
||||
if ext == ".py" and env.WINDOWS:
|
||||
exts = [".py", ".pyw"]
|
||||
if ext == '.py' and env.WINDOWS:
|
||||
exts = ['.py', '.pyw']
|
||||
else:
|
||||
exts = [ext]
|
||||
|
||||
|
|
@ -63,12 +70,12 @@ def get_python_source(filename: str) -> str:
|
|||
raise NoSource(f"No source for code: '{filename}'.")
|
||||
|
||||
# Replace \f because of http://bugs.python.org/issue19035
|
||||
source_bytes = source_bytes.replace(b"\f", b" ")
|
||||
source = source_bytes.decode(source_encoding(source_bytes), "replace")
|
||||
source_bytes = source_bytes.replace(b'\f', b' ')
|
||||
source = source_bytes.decode(source_encoding(source_bytes), 'replace')
|
||||
|
||||
# Python code should always end with a line with a newline.
|
||||
if source and source[-1] != "\n":
|
||||
source += "\n"
|
||||
if source and source[-1] != '\n':
|
||||
source += '\n'
|
||||
|
||||
return source
|
||||
|
||||
|
|
@ -103,11 +110,11 @@ def source_for_file(filename: str) -> str:
|
|||
file to attribute it to.
|
||||
|
||||
"""
|
||||
if filename.endswith(".py"):
|
||||
if filename.endswith('.py'):
|
||||
# .py files are themselves source files.
|
||||
return filename
|
||||
|
||||
elif filename.endswith((".pyc", ".pyo")):
|
||||
elif filename.endswith(('.pyc', '.pyo')):
|
||||
# Bytecode files probably have source files near them.
|
||||
py_filename = filename[:-1]
|
||||
if os.path.exists(py_filename):
|
||||
|
|
@ -115,7 +122,7 @@ def source_for_file(filename: str) -> str:
|
|||
return py_filename
|
||||
if env.WINDOWS:
|
||||
# On Windows, it could be a .pyw file.
|
||||
pyw_filename = py_filename + "w"
|
||||
pyw_filename = py_filename + 'w'
|
||||
if os.path.exists(pyw_filename):
|
||||
return pyw_filename
|
||||
# Didn't find source, but it's probably the .py file we want.
|
||||
|
|
@ -127,12 +134,12 @@ def source_for_file(filename: str) -> str:
|
|||
|
||||
def source_for_morf(morf: TMorf) -> str:
|
||||
"""Get the source filename for the module-or-file `morf`."""
|
||||
if hasattr(morf, "__file__") and morf.__file__:
|
||||
if hasattr(morf, '__file__') and morf.__file__:
|
||||
filename = morf.__file__
|
||||
elif isinstance(morf, types.ModuleType):
|
||||
# A module should have had .__file__, otherwise we can't use it.
|
||||
# This could be a PEP-420 namespace package.
|
||||
raise CoverageException(f"Module {morf} has no file")
|
||||
raise CoverageException(f'Module {morf} has no file')
|
||||
else:
|
||||
filename = morf
|
||||
|
||||
|
|
@ -157,11 +164,11 @@ class PythonFileReporter(FileReporter):
|
|||
fname = canonical_filename(filename)
|
||||
super().__init__(fname)
|
||||
|
||||
if hasattr(morf, "__name__"):
|
||||
name = morf.__name__.replace(".", os.sep)
|
||||
if os.path.basename(filename).startswith("__init__."):
|
||||
name += os.sep + "__init__"
|
||||
name += ".py"
|
||||
if hasattr(morf, '__name__'):
|
||||
name = morf.__name__.replace('.', os.sep)
|
||||
if os.path.basename(filename).startswith('__init__.'):
|
||||
name += os.sep + '__init__'
|
||||
name += '.py'
|
||||
else:
|
||||
name = relative_filename(filename)
|
||||
self.relname = name
|
||||
|
|
@ -171,7 +178,7 @@ class PythonFileReporter(FileReporter):
|
|||
self._excluded = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<PythonFileReporter {self.filename!r}>"
|
||||
return f'<PythonFileReporter {self.filename!r}>'
|
||||
|
||||
def relative_filename(self) -> str:
|
||||
return self.relname
|
||||
|
|
@ -183,7 +190,7 @@ class PythonFileReporter(FileReporter):
|
|||
if self._parser is None:
|
||||
self._parser = PythonParser(
|
||||
filename=self.filename,
|
||||
exclude=self.coverage._exclude_regex("exclude"),
|
||||
exclude=self.coverage._exclude_regex('exclude'),
|
||||
)
|
||||
self._parser.parse_source()
|
||||
return self._parser
|
||||
|
|
@ -244,7 +251,7 @@ class PythonFileReporter(FileReporter):
|
|||
_, ext = os.path.splitext(self.filename)
|
||||
|
||||
# Anything named *.py* should be Python.
|
||||
if ext.startswith(".py"):
|
||||
if ext.startswith('.py'):
|
||||
return True
|
||||
# A file with no extension should be Python.
|
||||
if not ext:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Raw data collector for coverage.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
|
|
@ -10,29 +8,37 @@ import dis
|
|||
import itertools
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from types import FrameType, ModuleType
|
||||
from typing import Any, Callable, Set, cast
|
||||
from types import FrameType
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Set
|
||||
|
||||
from coverage import env
|
||||
from coverage.types import (
|
||||
TArc, TFileDisposition, TLineNo, TTraceData, TTraceFileData, TTraceFn,
|
||||
TracerCore, TWarnFn,
|
||||
)
|
||||
from coverage.types import TArc
|
||||
from coverage.types import TFileDisposition
|
||||
from coverage.types import TLineNo
|
||||
from coverage.types import TracerCore
|
||||
from coverage.types import TTraceData
|
||||
from coverage.types import TTraceFileData
|
||||
from coverage.types import TTraceFn
|
||||
from coverage.types import TWarnFn
|
||||
|
||||
# We need the YIELD_VALUE opcode below, in a comparison-friendly form.
|
||||
# PYVERSIONS: RESUME is new in Python3.11
|
||||
RESUME = dis.opmap.get("RESUME")
|
||||
RETURN_VALUE = dis.opmap["RETURN_VALUE"]
|
||||
RESUME = dis.opmap.get('RESUME')
|
||||
RETURN_VALUE = dis.opmap['RETURN_VALUE']
|
||||
if RESUME is None:
|
||||
YIELD_VALUE = dis.opmap["YIELD_VALUE"]
|
||||
YIELD_FROM = dis.opmap["YIELD_FROM"]
|
||||
YIELD_VALUE = dis.opmap['YIELD_VALUE']
|
||||
YIELD_FROM = dis.opmap['YIELD_FROM']
|
||||
YIELD_FROM_OFFSET = 0 if env.PYPY else 2
|
||||
|
||||
# When running meta-coverage, this file can try to trace itself, which confuses
|
||||
# everything. Don't trace ourselves.
|
||||
|
||||
THIS_FILE = __file__.rstrip("co")
|
||||
THIS_FILE = __file__.rstrip('co')
|
||||
|
||||
|
||||
class PyTracer(TracerCore):
|
||||
"""Python implementation of the raw data tracer."""
|
||||
|
|
@ -92,7 +98,7 @@ class PyTracer(TracerCore):
|
|||
|
||||
self.in_atexit = False
|
||||
# On exit, self.in_atexit = True
|
||||
atexit.register(setattr, self, "in_atexit", True)
|
||||
atexit.register(setattr, self, 'in_atexit', True)
|
||||
|
||||
# Cache a bound method on the instance, so that we don't have to
|
||||
# re-create a bound method object all the time.
|
||||
|
|
@ -101,26 +107,28 @@ class PyTracer(TracerCore):
|
|||
def __repr__(self) -> str:
|
||||
points = sum(len(v) for v in self.data.values())
|
||||
files = len(self.data)
|
||||
return f"<PyTracer at {id(self):#x}: {points} data points in {files} files>"
|
||||
return f'<PyTracer at {id(self):#x}: {points} data points in {files} files>'
|
||||
|
||||
def log(self, marker: str, *args: Any) -> None:
|
||||
"""For hard-core logging of what this tracer is doing."""
|
||||
with open("/tmp/debug_trace.txt", "a") as f:
|
||||
f.write(f"{marker} {self.id}[{len(self.data_stack)}]")
|
||||
with open('/tmp/debug_trace.txt', 'a') as f:
|
||||
f.write(f'{marker} {self.id}[{len(self.data_stack)}]')
|
||||
if 0: # if you want thread ids..
|
||||
f.write(".{:x}.{:x}".format( # type: ignore[unreachable]
|
||||
self.thread.ident,
|
||||
self.threading.current_thread().ident,
|
||||
))
|
||||
f.write(" {}".format(" ".join(map(str, args))))
|
||||
f.write(
|
||||
'.{:x}.{:x}'.format( # type: ignore[unreachable]
|
||||
self.thread.ident,
|
||||
self.threading.current_thread().ident,
|
||||
),
|
||||
)
|
||||
f.write(' {}'.format(' '.join(map(str, args))))
|
||||
if 0: # if you want callers..
|
||||
f.write(" | ") # type: ignore[unreachable]
|
||||
stack = " / ".join(
|
||||
(fname or "???").rpartition("/")[-1]
|
||||
f.write(' | ') # type: ignore[unreachable]
|
||||
stack = ' / '.join(
|
||||
(fname or '???').rpartition('/')[-1]
|
||||
for _, fname, _, _ in self.data_stack
|
||||
)
|
||||
f.write(stack)
|
||||
f.write("\n")
|
||||
f.write('\n')
|
||||
|
||||
def _trace(
|
||||
self,
|
||||
|
|
@ -142,9 +150,9 @@ class PyTracer(TracerCore):
|
|||
# thread, let's deactivate ourselves now.
|
||||
if 0:
|
||||
f = frame # type: ignore[unreachable]
|
||||
self.log("---\nX", f.f_code.co_filename, f.f_lineno)
|
||||
self.log('---\nX', f.f_code.co_filename, f.f_lineno)
|
||||
while f:
|
||||
self.log(">", f.f_code.co_filename, f.f_lineno, f.f_code.co_name, f.f_trace)
|
||||
self.log('>', f.f_code.co_filename, f.f_lineno, f.f_code.co_name, f.f_trace)
|
||||
f = f.f_back
|
||||
sys.settrace(None)
|
||||
try:
|
||||
|
|
@ -153,7 +161,7 @@ class PyTracer(TracerCore):
|
|||
)
|
||||
except IndexError:
|
||||
self.log(
|
||||
"Empty stack!",
|
||||
'Empty stack!',
|
||||
frame.f_code.co_filename,
|
||||
frame.f_lineno,
|
||||
frame.f_code.co_name,
|
||||
|
|
@ -163,7 +171,7 @@ class PyTracer(TracerCore):
|
|||
# if event != "call" and frame.f_code.co_filename != self.cur_file_name:
|
||||
# self.log("---\n*", frame.f_code.co_filename, self.cur_file_name, frame.f_lineno)
|
||||
|
||||
if event == "call":
|
||||
if event == 'call':
|
||||
# Should we start a new context?
|
||||
if self.should_start_context and self.context is None:
|
||||
context_maybe = self.should_start_context(frame)
|
||||
|
|
@ -225,13 +233,13 @@ class PyTracer(TracerCore):
|
|||
oparg = frame.f_code.co_code[frame.f_lasti + 1]
|
||||
real_call = (oparg == 0)
|
||||
else:
|
||||
real_call = (getattr(frame, "f_lasti", -1) < 0)
|
||||
real_call = (getattr(frame, 'f_lasti', -1) < 0)
|
||||
if real_call:
|
||||
self.last_line = -frame.f_code.co_firstlineno
|
||||
else:
|
||||
self.last_line = frame.f_lineno
|
||||
|
||||
elif event == "line":
|
||||
elif event == 'line':
|
||||
# Record an executed line.
|
||||
if self.cur_file_data is not None:
|
||||
flineno: TLineNo = frame.f_lineno
|
||||
|
|
@ -242,7 +250,7 @@ class PyTracer(TracerCore):
|
|||
cast(Set[TLineNo], self.cur_file_data).add(flineno)
|
||||
self.last_line = flineno
|
||||
|
||||
elif event == "return":
|
||||
elif event == 'return':
|
||||
if self.trace_arcs and self.cur_file_data:
|
||||
# Record an arc leaving the function, but beware that a
|
||||
# "return" event might just mean yielding from a generator.
|
||||
|
|
@ -322,15 +330,15 @@ class PyTracer(TracerCore):
|
|||
# has changed to None. Metacoverage also messes this up, so don't
|
||||
# warn if we are measuring ourselves.
|
||||
suppress_warning = (
|
||||
(env.PYPY and self.in_atexit and tf is None)
|
||||
or env.METACOV
|
||||
(env.PYPY and self.in_atexit and tf is None) or
|
||||
env.METACOV
|
||||
)
|
||||
if self.warn and not suppress_warning:
|
||||
if tf != self._cached_bound_method_trace: # pylint: disable=comparison-with-callable
|
||||
self.warn(
|
||||
"Trace function changed, data is likely wrong: " +
|
||||
f"{tf!r} != {self._cached_bound_method_trace!r}",
|
||||
slug="trace-changed",
|
||||
'Trace function changed, data is likely wrong: ' +
|
||||
f'{tf!r} != {self._cached_bound_method_trace!r}',
|
||||
slug='trace-changed',
|
||||
)
|
||||
|
||||
def activity(self) -> bool:
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Summary reporting"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import IO
|
||||
from typing import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from typing import Any, IO, Iterable, TYPE_CHECKING
|
||||
|
||||
from coverage.exceptions import ConfigError, NoDataError
|
||||
from coverage.exceptions import ConfigError
|
||||
from coverage.exceptions import NoDataError
|
||||
from coverage.misc import human_sorted_items
|
||||
from coverage.plugin import FileReporter
|
||||
from coverage.report_core import get_analysis_to_report
|
||||
from coverage.results import Analysis, Numbers
|
||||
from coverage.results import Analysis
|
||||
from coverage.results import Numbers
|
||||
from coverage.types import TMorf
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -28,9 +30,9 @@ class SummaryReporter:
|
|||
self.config = self.coverage.config
|
||||
self.branches = coverage.get_data().has_arcs()
|
||||
self.outfile: IO[str] | None = None
|
||||
self.output_format = self.config.format or "text"
|
||||
if self.output_format not in {"text", "markdown", "total"}:
|
||||
raise ConfigError(f"Unknown report format choice: {self.output_format!r}")
|
||||
self.output_format = self.config.format or 'text'
|
||||
if self.output_format not in {'text', 'markdown', 'total'}:
|
||||
raise ConfigError(f'Unknown report format choice: {self.output_format!r}')
|
||||
self.fr_analysis: list[tuple[FileReporter, Analysis]] = []
|
||||
self.skipped_count = 0
|
||||
self.empty_count = 0
|
||||
|
|
@ -40,11 +42,11 @@ class SummaryReporter:
|
|||
"""Write a line to the output, adding a newline."""
|
||||
assert self.outfile is not None
|
||||
self.outfile.write(line.rstrip())
|
||||
self.outfile.write("\n")
|
||||
self.outfile.write('\n')
|
||||
|
||||
def write_items(self, items: Iterable[str]) -> None:
|
||||
"""Write a list of strings, joined together."""
|
||||
self.write("".join(items))
|
||||
self.write(''.join(items))
|
||||
|
||||
def _report_text(
|
||||
self,
|
||||
|
|
@ -63,34 +65,36 @@ class SummaryReporter:
|
|||
"""
|
||||
# Prepare the formatting strings, header, and column sorting.
|
||||
max_name = max([len(line[0]) for line in lines_values] + [5]) + 1
|
||||
max_n = max(len(total_line[header.index("Cover")]) + 2, len(" Cover")) + 1
|
||||
max_n = max([max_n] + [len(line[header.index("Cover")]) + 2 for line in lines_values])
|
||||
max_n = max(len(total_line[header.index('Cover')]) + 2, len(' Cover')) + 1
|
||||
max_n = max([max_n] + [len(line[header.index('Cover')]) + 2 for line in lines_values])
|
||||
formats = dict(
|
||||
Name="{:{name_len}}",
|
||||
Stmts="{:>7}",
|
||||
Miss="{:>7}",
|
||||
Branch="{:>7}",
|
||||
BrPart="{:>7}",
|
||||
Cover="{:>{n}}",
|
||||
Missing="{:>10}",
|
||||
Name='{:{name_len}}',
|
||||
Stmts='{:>7}',
|
||||
Miss='{:>7}',
|
||||
Branch='{:>7}',
|
||||
BrPart='{:>7}',
|
||||
Cover='{:>{n}}',
|
||||
Missing='{:>10}',
|
||||
)
|
||||
header_items = [
|
||||
formats[item].format(item, name_len=max_name, n=max_n)
|
||||
for item in header
|
||||
]
|
||||
header_str = "".join(header_items)
|
||||
rule = "-" * len(header_str)
|
||||
header_str = ''.join(header_items)
|
||||
rule = '-' * len(header_str)
|
||||
|
||||
# Write the header
|
||||
self.write(header_str)
|
||||
self.write(rule)
|
||||
|
||||
formats.update(dict(Cover="{:>{n}}%"), Missing=" {:9}")
|
||||
formats.update(dict(Cover='{:>{n}}%'), Missing=' {:9}')
|
||||
for values in lines_values:
|
||||
# build string with line values
|
||||
line_items = [
|
||||
formats[item].format(str(value),
|
||||
name_len=max_name, n=max_n-1) for item, value in zip(header, values)
|
||||
formats[item].format(
|
||||
str(value),
|
||||
name_len=max_name, n=max_n - 1,
|
||||
) for item, value in zip(header, values)
|
||||
]
|
||||
self.write_items(line_items)
|
||||
|
||||
|
|
@ -99,8 +103,10 @@ class SummaryReporter:
|
|||
self.write(rule)
|
||||
|
||||
line_items = [
|
||||
formats[item].format(str(value),
|
||||
name_len=max_name, n=max_n-1) for item, value in zip(header, total_line)
|
||||
formats[item].format(
|
||||
str(value),
|
||||
name_len=max_name, n=max_n - 1,
|
||||
) for item, value in zip(header, total_line)
|
||||
]
|
||||
self.write_items(line_items)
|
||||
|
||||
|
|
@ -123,22 +129,23 @@ class SummaryReporter:
|
|||
|
||||
"""
|
||||
# Prepare the formatting strings, header, and column sorting.
|
||||
max_name = max((len(line[0].replace("_", "\\_")) for line in lines_values), default=0)
|
||||
max_name = max(max_name, len("**TOTAL**")) + 1
|
||||
max_name = max((len(line[0].replace('_', '\\_')) for line in lines_values), default=0)
|
||||
max_name = max(max_name, len('**TOTAL**')) + 1
|
||||
formats = dict(
|
||||
Name="| {:{name_len}}|",
|
||||
Stmts="{:>9} |",
|
||||
Miss="{:>9} |",
|
||||
Branch="{:>9} |",
|
||||
BrPart="{:>9} |",
|
||||
Cover="{:>{n}} |",
|
||||
Missing="{:>10} |",
|
||||
Name='| {:{name_len}}|',
|
||||
Stmts='{:>9} |',
|
||||
Miss='{:>9} |',
|
||||
Branch='{:>9} |',
|
||||
BrPart='{:>9} |',
|
||||
Cover='{:>{n}} |',
|
||||
Missing='{:>10} |',
|
||||
)
|
||||
max_n = max(len(total_line[header.index("Cover")]) + 6, len(" Cover "))
|
||||
max_n = max(len(total_line[header.index('Cover')]) + 6, len(' Cover '))
|
||||
header_items = [formats[item].format(item, name_len=max_name, n=max_n) for item in header]
|
||||
header_str = "".join(header_items)
|
||||
rule_str = "|" + " ".join(["- |".rjust(len(header_items[0])-1, "-")] +
|
||||
["-: |".rjust(len(item)-1, "-") for item in header_items[1:]],
|
||||
header_str = ''.join(header_items)
|
||||
rule_str = '|' + ' '.join(
|
||||
['- |'.rjust(len(header_items[0]) - 1, '-')] +
|
||||
['-: |'.rjust(len(item) - 1, '-') for item in header_items[1:]],
|
||||
)
|
||||
|
||||
# Write the header
|
||||
|
|
@ -147,23 +154,23 @@ class SummaryReporter:
|
|||
|
||||
for values in lines_values:
|
||||
# build string with line values
|
||||
formats.update(dict(Cover="{:>{n}}% |"))
|
||||
formats.update(dict(Cover='{:>{n}}% |'))
|
||||
line_items = [
|
||||
formats[item].format(str(value).replace("_", "\\_"), name_len=max_name, n=max_n-1)
|
||||
formats[item].format(str(value).replace('_', '\\_'), name_len=max_name, n=max_n - 1)
|
||||
for item, value in zip(header, values)
|
||||
]
|
||||
self.write_items(line_items)
|
||||
|
||||
# Write the TOTAL line
|
||||
formats.update(dict(Name="|{:>{name_len}} |", Cover="{:>{n}} |"))
|
||||
formats.update(dict(Name='|{:>{name_len}} |', Cover='{:>{n}} |'))
|
||||
total_line_items: list[str] = []
|
||||
for item, value in zip(header, total_line):
|
||||
if value == "":
|
||||
if value == '':
|
||||
insert = value
|
||||
elif item == "Cover":
|
||||
insert = f" **{value}%**"
|
||||
elif item == 'Cover':
|
||||
insert = f' **{value}%**'
|
||||
else:
|
||||
insert = f" **{value}**"
|
||||
insert = f' **{value}**'
|
||||
total_line_items += formats[item].format(insert, name_len=max_name, n=max_n)
|
||||
self.write_items(total_line_items)
|
||||
for end_line in end_lines:
|
||||
|
|
@ -182,9 +189,9 @@ class SummaryReporter:
|
|||
self.report_one_file(fr, analysis)
|
||||
|
||||
if not self.total.n_files and not self.skipped_count:
|
||||
raise NoDataError("No data to report.")
|
||||
raise NoDataError('No data to report.')
|
||||
|
||||
if self.output_format == "total":
|
||||
if self.output_format == 'total':
|
||||
self.write(self.total.pc_covered_str)
|
||||
else:
|
||||
self.tabular_report()
|
||||
|
|
@ -194,12 +201,12 @@ class SummaryReporter:
|
|||
def tabular_report(self) -> None:
|
||||
"""Writes tabular report formats."""
|
||||
# Prepare the header line and column sorting.
|
||||
header = ["Name", "Stmts", "Miss"]
|
||||
header = ['Name', 'Stmts', 'Miss']
|
||||
if self.branches:
|
||||
header += ["Branch", "BrPart"]
|
||||
header += ["Cover"]
|
||||
header += ['Branch', 'BrPart']
|
||||
header += ['Cover']
|
||||
if self.config.show_missing:
|
||||
header += ["Missing"]
|
||||
header += ['Missing']
|
||||
|
||||
column_order = dict(name=0, stmts=1, miss=2, cover=-1)
|
||||
if self.branches:
|
||||
|
|
@ -221,17 +228,17 @@ class SummaryReporter:
|
|||
lines_values.append(args)
|
||||
|
||||
# Line sorting.
|
||||
sort_option = (self.config.sort or "name").lower()
|
||||
sort_option = (self.config.sort or 'name').lower()
|
||||
reverse = False
|
||||
if sort_option[0] == "-":
|
||||
if sort_option[0] == '-':
|
||||
reverse = True
|
||||
sort_option = sort_option[1:]
|
||||
elif sort_option[0] == "+":
|
||||
elif sort_option[0] == '+':
|
||||
sort_option = sort_option[1:]
|
||||
sort_idx = column_order.get(sort_option)
|
||||
if sort_idx is None:
|
||||
raise ConfigError(f"Invalid sorting option: {self.config.sort!r}")
|
||||
if sort_option == "name":
|
||||
raise ConfigError(f'Invalid sorting option: {self.config.sort!r}')
|
||||
if sort_option == 'name':
|
||||
lines_values = human_sorted_items(lines_values, reverse=reverse)
|
||||
else:
|
||||
lines_values.sort(
|
||||
|
|
@ -240,25 +247,25 @@ class SummaryReporter:
|
|||
)
|
||||
|
||||
# Calculate total if we had at least one file.
|
||||
total_line = ["TOTAL", self.total.n_statements, self.total.n_missing]
|
||||
total_line = ['TOTAL', self.total.n_statements, self.total.n_missing]
|
||||
if self.branches:
|
||||
total_line += [self.total.n_branches, self.total.n_partial_branches]
|
||||
total_line += [self.total.pc_covered_str]
|
||||
if self.config.show_missing:
|
||||
total_line += [""]
|
||||
total_line += ['']
|
||||
|
||||
# Create other final lines.
|
||||
end_lines = []
|
||||
if self.config.skip_covered and self.skipped_count:
|
||||
file_suffix = "s" if self.skipped_count>1 else ""
|
||||
file_suffix = 's' if self.skipped_count > 1 else ''
|
||||
end_lines.append(
|
||||
f"\n{self.skipped_count} file{file_suffix} skipped due to complete coverage.",
|
||||
f'\n{self.skipped_count} file{file_suffix} skipped due to complete coverage.',
|
||||
)
|
||||
if self.config.skip_empty and self.empty_count:
|
||||
file_suffix = "s" if self.empty_count > 1 else ""
|
||||
end_lines.append(f"\n{self.empty_count} empty file{file_suffix} skipped.")
|
||||
file_suffix = 's' if self.empty_count > 1 else ''
|
||||
end_lines.append(f'\n{self.empty_count} empty file{file_suffix} skipped.')
|
||||
|
||||
if self.output_format == "markdown":
|
||||
if self.output_format == 'markdown':
|
||||
formatter = self._report_markdown
|
||||
else:
|
||||
formatter = self._report_text
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Reporter foundation for coverage.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Callable
|
||||
from typing import IO
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import Protocol
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from typing import (
|
||||
Callable, Iterable, Iterator, IO, Protocol, TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from coverage.exceptions import NoDataError, NotPython
|
||||
from coverage.files import prep_patterns, GlobMatcher
|
||||
from coverage.misc import ensure_dir_for_file, file_be_gone
|
||||
from coverage.exceptions import NoDataError
|
||||
from coverage.exceptions import NotPython
|
||||
from coverage.files import GlobMatcher
|
||||
from coverage.files import prep_patterns
|
||||
from coverage.misc import ensure_dir_for_file
|
||||
from coverage.misc import file_be_gone
|
||||
from coverage.plugin import FileReporter
|
||||
from coverage.results import Analysis
|
||||
from coverage.types import TMorf
|
||||
|
|
@ -46,21 +49,21 @@ def render_report(
|
|||
file_to_close = None
|
||||
delete_file = False
|
||||
|
||||
if output_path == "-":
|
||||
if output_path == '-':
|
||||
outfile = sys.stdout
|
||||
else:
|
||||
# Ensure that the output directory is created; done here because this
|
||||
# report pre-opens the output file. HtmlReporter does this on its own
|
||||
# because its task is more complex, being multiple files.
|
||||
ensure_dir_for_file(output_path)
|
||||
outfile = open(output_path, "w", encoding="utf-8")
|
||||
outfile = open(output_path, 'w', encoding='utf-8')
|
||||
file_to_close = outfile
|
||||
delete_file = True
|
||||
|
||||
try:
|
||||
ret = reporter.report(morfs, outfile=outfile)
|
||||
if file_to_close is not None:
|
||||
msgfn(f"Wrote {reporter.report_type} to {output_path}")
|
||||
msgfn(f'Wrote {reporter.report_type} to {output_path}')
|
||||
delete_file = False
|
||||
return ret
|
||||
finally:
|
||||
|
|
@ -85,15 +88,15 @@ def get_analysis_to_report(
|
|||
config = coverage.config
|
||||
|
||||
if config.report_include:
|
||||
matcher = GlobMatcher(prep_patterns(config.report_include), "report_include")
|
||||
matcher = GlobMatcher(prep_patterns(config.report_include), 'report_include')
|
||||
file_reporters = [fr for fr in file_reporters if matcher.match(fr.filename)]
|
||||
|
||||
if config.report_omit:
|
||||
matcher = GlobMatcher(prep_patterns(config.report_omit), "report_omit")
|
||||
matcher = GlobMatcher(prep_patterns(config.report_omit), 'report_omit')
|
||||
file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)]
|
||||
|
||||
if not file_reporters:
|
||||
raise NoDataError("No data to report.")
|
||||
raise NoDataError('No data to report.')
|
||||
|
||||
for fr in sorted(file_reporters):
|
||||
try:
|
||||
|
|
@ -106,13 +109,13 @@ def get_analysis_to_report(
|
|||
if fr.should_be_python(): # type: ignore[attr-defined]
|
||||
if config.ignore_errors:
|
||||
msg = f"Couldn't parse Python file '{fr.filename}'"
|
||||
coverage._warn(msg, slug="couldnt-parse")
|
||||
coverage._warn(msg, slug='couldnt-parse')
|
||||
else:
|
||||
raise
|
||||
except Exception as exc:
|
||||
if config.ignore_errors:
|
||||
msg = f"Couldn't parse '{fr.filename}': {exc}".rstrip()
|
||||
coverage._warn(msg, slug="couldnt-parse")
|
||||
coverage._warn(msg, slug='couldnt-parse')
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Results of coverage measurement."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
|
||||
from typing import Callable, Iterable, TYPE_CHECKING
|
||||
from typing import Callable
|
||||
from typing import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from coverage.debug import auto_repr
|
||||
from coverage.exceptions import ConfigError
|
||||
from coverage.misc import nice_pair
|
||||
from coverage.types import TArc, TLineNo
|
||||
from coverage.types import TArc
|
||||
from coverage.types import TLineNo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from coverage.data import CoverageData
|
||||
|
|
@ -48,8 +48,8 @@ class Analysis:
|
|||
self.no_branch = self.file_reporter.no_branch_lines()
|
||||
n_branches = self._total_branches()
|
||||
mba = self.missing_branch_arcs()
|
||||
n_partial_branches = sum(len(v) for k,v in mba.items() if k not in self.missing)
|
||||
n_missing_branches = sum(len(v) for k,v in mba.items())
|
||||
n_partial_branches = sum(len(v) for k, v in mba.items() if k not in self.missing)
|
||||
n_missing_branches = sum(len(v) for k, v in mba.items())
|
||||
else:
|
||||
self._arc_possibilities = []
|
||||
self.exit_counts = {}
|
||||
|
|
@ -103,9 +103,9 @@ class Analysis:
|
|||
executed = self.arcs_executed()
|
||||
missing = (
|
||||
p for p in possible
|
||||
if p not in executed
|
||||
and p[0] not in self.no_branch
|
||||
and p[1] not in self.excluded
|
||||
if p not in executed and
|
||||
p[0] not in self.no_branch and
|
||||
p[1] not in self.excluded
|
||||
)
|
||||
return sorted(missing)
|
||||
|
||||
|
|
@ -120,15 +120,15 @@ class Analysis:
|
|||
# make sure we have at least one positive value.
|
||||
unpredicted = (
|
||||
e for e in executed
|
||||
if e not in possible
|
||||
and e[0] != e[1]
|
||||
and (e[0] > 0 or e[1] > 0)
|
||||
if e not in possible and
|
||||
e[0] != e[1] and
|
||||
(e[0] > 0 or e[1] > 0)
|
||||
)
|
||||
return sorted(unpredicted)
|
||||
|
||||
def _branch_lines(self) -> list[TLineNo]:
|
||||
"""Returns a list of line numbers that have more than one exit."""
|
||||
return [l1 for l1,count in self.exit_counts.items() if count > 1]
|
||||
return [l1 for l1, count in self.exit_counts.items() if count > 1]
|
||||
|
||||
def _total_branches(self) -> int:
|
||||
"""How many total branches are there?"""
|
||||
|
|
@ -264,7 +264,7 @@ class Numbers:
|
|||
pc = self._near100
|
||||
else:
|
||||
pc = round(pc, self._precision)
|
||||
return "%.*f" % (self._precision, pc)
|
||||
return '%.*f' % (self._precision, pc)
|
||||
|
||||
def pc_str_width(self) -> int:
|
||||
"""How many characters wide can pc_covered_str be?"""
|
||||
|
|
@ -356,10 +356,10 @@ def format_lines(
|
|||
for line, exits in line_exits:
|
||||
for ex in sorted(exits):
|
||||
if line not in lines and ex not in lines:
|
||||
dest = (ex if ex > 0 else "exit")
|
||||
line_items.append((line, f"{line}->{dest}"))
|
||||
dest = (ex if ex > 0 else 'exit')
|
||||
line_items.append((line, f'{line}->{dest}'))
|
||||
|
||||
ret = ", ".join(t[-1] for t in sorted(line_items))
|
||||
ret = ', '.join(t[-1] for t in sorted(line_items))
|
||||
return ret
|
||||
|
||||
|
||||
|
|
@ -375,7 +375,7 @@ def should_fail_under(total: float, fail_under: float, precision: int) -> bool:
|
|||
"""
|
||||
# We can never achieve higher than 100% coverage, or less than zero.
|
||||
if not (0 <= fail_under <= 100.0):
|
||||
msg = f"fail_under={fail_under} is invalid. Must be between 0 and 100."
|
||||
msg = f'fail_under={fail_under} is invalid. Must be between 0 and 100.'
|
||||
raise ConfigError(msg)
|
||||
|
||||
# Special case for fail_under=100, it must really be 100.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""SQLite coverage data."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
|
|
@ -19,19 +17,29 @@ import sys
|
|||
import textwrap
|
||||
import threading
|
||||
import zlib
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import Collection
|
||||
from typing import Mapping
|
||||
from typing import Sequence
|
||||
|
||||
from typing import (
|
||||
cast, Any, Collection, Mapping,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
from coverage.debug import NoDebugging, auto_repr
|
||||
from coverage.exceptions import CoverageException, DataError
|
||||
from coverage.debug import auto_repr
|
||||
from coverage.debug import NoDebugging
|
||||
from coverage.exceptions import CoverageException
|
||||
from coverage.exceptions import DataError
|
||||
from coverage.files import PathAliases
|
||||
from coverage.misc import file_be_gone, isolate_module
|
||||
from coverage.numbits import numbits_to_nums, numbits_union, nums_to_numbits
|
||||
from coverage.misc import file_be_gone
|
||||
from coverage.misc import isolate_module
|
||||
from coverage.numbits import numbits_to_nums
|
||||
from coverage.numbits import numbits_union
|
||||
from coverage.numbits import nums_to_numbits
|
||||
from coverage.sqlitedb import SqliteDb
|
||||
from coverage.types import AnyCallable, FilePath, TArc, TDebugCtl, TLineNo, TWarnFn
|
||||
from coverage.types import AnyCallable
|
||||
from coverage.types import FilePath
|
||||
from coverage.types import TArc
|
||||
from coverage.types import TDebugCtl
|
||||
from coverage.types import TLineNo
|
||||
from coverage.types import TWarnFn
|
||||
from coverage.version import __version__
|
||||
|
||||
os = isolate_module(os)
|
||||
|
|
@ -112,15 +120,16 @@ CREATE TABLE tracer (
|
|||
);
|
||||
"""
|
||||
|
||||
|
||||
def _locked(method: AnyCallable) -> AnyCallable:
|
||||
"""A decorator for methods that should hold self._lock."""
|
||||
@functools.wraps(method)
|
||||
def _wrapped(self: CoverageData, *args: Any, **kwargs: Any) -> Any:
|
||||
if self._debug.should("lock"):
|
||||
self._debug.write(f"Locking {self._lock!r} for {method.__name__}")
|
||||
if self._debug.should('lock'):
|
||||
self._debug.write(f'Locking {self._lock!r} for {method.__name__}')
|
||||
with self._lock:
|
||||
if self._debug.should("lock"):
|
||||
self._debug.write(f"Locked {self._lock!r} for {method.__name__}")
|
||||
if self._debug.should('lock'):
|
||||
self._debug.write(f'Locked {self._lock!r} for {method.__name__}')
|
||||
return method(self, *args, **kwargs)
|
||||
return _wrapped
|
||||
|
||||
|
|
@ -233,7 +242,7 @@ class CoverageData:
|
|||
|
||||
"""
|
||||
self._no_disk = no_disk
|
||||
self._basename = os.path.abspath(basename or ".coverage")
|
||||
self._basename = os.path.abspath(basename or '.coverage')
|
||||
self._suffix = suffix
|
||||
self._warn = warn
|
||||
self._debug = debug or NoDebugging()
|
||||
|
|
@ -262,12 +271,12 @@ class CoverageData:
|
|||
def _choose_filename(self) -> None:
|
||||
"""Set self._filename based on inited attributes."""
|
||||
if self._no_disk:
|
||||
self._filename = ":memory:"
|
||||
self._filename = ':memory:'
|
||||
else:
|
||||
self._filename = self._basename
|
||||
suffix = filename_suffix(self._suffix)
|
||||
if suffix:
|
||||
self._filename += "." + suffix
|
||||
self._filename += '.' + suffix
|
||||
|
||||
def _reset(self) -> None:
|
||||
"""Reset our attributes."""
|
||||
|
|
@ -281,8 +290,8 @@ class CoverageData:
|
|||
|
||||
def _open_db(self) -> None:
|
||||
"""Open an existing db file, and read its metadata."""
|
||||
if self._debug.should("dataio"):
|
||||
self._debug.write(f"Opening data file {self._filename!r}")
|
||||
if self._debug.should('dataio'):
|
||||
self._debug.write(f'Opening data file {self._filename!r}')
|
||||
self._dbs[threading.get_ident()] = SqliteDb(self._filename, self._debug)
|
||||
self._read_db()
|
||||
|
||||
|
|
@ -290,10 +299,10 @@ class CoverageData:
|
|||
"""Read the metadata from a database so that we are ready to use it."""
|
||||
with self._dbs[threading.get_ident()] as db:
|
||||
try:
|
||||
row = db.execute_one("select version from coverage_schema")
|
||||
row = db.execute_one('select version from coverage_schema')
|
||||
assert row is not None
|
||||
except Exception as exc:
|
||||
if "no such table: coverage_schema" in str(exc):
|
||||
if 'no such table: coverage_schema' in str(exc):
|
||||
self._init_db(db)
|
||||
else:
|
||||
raise DataError(
|
||||
|
|
@ -315,28 +324,28 @@ class CoverageData:
|
|||
self._has_arcs = bool(int(row[0]))
|
||||
self._has_lines = not self._has_arcs
|
||||
|
||||
with db.execute("select id, path from file") as cur:
|
||||
with db.execute('select id, path from file') as cur:
|
||||
for file_id, path in cur:
|
||||
self._file_map[path] = file_id
|
||||
|
||||
def _init_db(self, db: SqliteDb) -> None:
|
||||
"""Write the initial contents of the database."""
|
||||
if self._debug.should("dataio"):
|
||||
self._debug.write(f"Initing data file {self._filename!r}")
|
||||
if self._debug.should('dataio'):
|
||||
self._debug.write(f'Initing data file {self._filename!r}')
|
||||
db.executescript(SCHEMA)
|
||||
db.execute_void("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,))
|
||||
db.execute_void('insert into coverage_schema (version) values (?)', (SCHEMA_VERSION,))
|
||||
|
||||
# When writing metadata, avoid information that will needlessly change
|
||||
# the hash of the data file, unless we're debugging processes.
|
||||
meta_data = [
|
||||
("version", __version__),
|
||||
('version', __version__),
|
||||
]
|
||||
if self._debug.should("process"):
|
||||
if self._debug.should('process'):
|
||||
meta_data.extend([
|
||||
("sys_argv", str(getattr(sys, "argv", None))),
|
||||
("when", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
|
||||
('sys_argv', str(getattr(sys, 'argv', None))),
|
||||
('when', datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')),
|
||||
])
|
||||
db.executemany_void("insert or ignore into meta (key, value) values (?, ?)", meta_data)
|
||||
db.executemany_void('insert or ignore into meta (key, value) values (?, ?)', meta_data)
|
||||
|
||||
def _connect(self) -> SqliteDb:
|
||||
"""Get the SqliteDb object to use."""
|
||||
|
|
@ -349,7 +358,7 @@ class CoverageData:
|
|||
return False
|
||||
try:
|
||||
with self._connect() as con:
|
||||
with con.execute("select * from file limit 1") as cur:
|
||||
with con.execute('select * from file limit 1') as cur:
|
||||
return bool(list(cur))
|
||||
except CoverageException:
|
||||
return False
|
||||
|
|
@ -371,11 +380,11 @@ class CoverageData:
|
|||
.. versionadded:: 5.0
|
||||
|
||||
"""
|
||||
if self._debug.should("dataio"):
|
||||
self._debug.write(f"Dumping data from data file {self._filename!r}")
|
||||
if self._debug.should('dataio'):
|
||||
self._debug.write(f'Dumping data from data file {self._filename!r}')
|
||||
with self._connect() as con:
|
||||
script = con.dump()
|
||||
return b"z" + zlib.compress(script.encode("utf-8"))
|
||||
return b'z' + zlib.compress(script.encode('utf-8'))
|
||||
|
||||
def loads(self, data: bytes) -> None:
|
||||
"""Deserialize data from :meth:`dumps`.
|
||||
|
|
@ -392,13 +401,13 @@ class CoverageData:
|
|||
.. versionadded:: 5.0
|
||||
|
||||
"""
|
||||
if self._debug.should("dataio"):
|
||||
self._debug.write(f"Loading data into data file {self._filename!r}")
|
||||
if data[:1] != b"z":
|
||||
if self._debug.should('dataio'):
|
||||
self._debug.write(f'Loading data into data file {self._filename!r}')
|
||||
if data[:1] != b'z':
|
||||
raise DataError(
|
||||
f"Unrecognized serialization: {data[:40]!r} (head of {len(data)} bytes)",
|
||||
f'Unrecognized serialization: {data[:40]!r} (head of {len(data)} bytes)',
|
||||
)
|
||||
script = zlib.decompress(data[1:]).decode("utf-8")
|
||||
script = zlib.decompress(data[1:]).decode('utf-8')
|
||||
self._dbs[threading.get_ident()] = db = SqliteDb(self._filename, self._debug)
|
||||
with db:
|
||||
db.executescript(script)
|
||||
|
|
@ -415,7 +424,7 @@ class CoverageData:
|
|||
if add:
|
||||
with self._connect() as con:
|
||||
self._file_map[filename] = con.execute_for_rowid(
|
||||
"insert or replace into file (path) values (?)",
|
||||
'insert or replace into file (path) values (?)',
|
||||
(filename,),
|
||||
)
|
||||
return self._file_map.get(filename)
|
||||
|
|
@ -425,7 +434,7 @@ class CoverageData:
|
|||
assert context is not None
|
||||
self._start_using()
|
||||
with self._connect() as con:
|
||||
row = con.execute_one("select id from context where context = ?", (context,))
|
||||
row = con.execute_one('select id from context where context = ?', (context,))
|
||||
if row is not None:
|
||||
return cast(int, row[0])
|
||||
else:
|
||||
|
|
@ -441,21 +450,21 @@ class CoverageData:
|
|||
.. versionadded:: 5.0
|
||||
|
||||
"""
|
||||
if self._debug.should("dataop"):
|
||||
self._debug.write(f"Setting coverage context: {context!r}")
|
||||
if self._debug.should('dataop'):
|
||||
self._debug.write(f'Setting coverage context: {context!r}')
|
||||
self._current_context = context
|
||||
self._current_context_id = None
|
||||
|
||||
def _set_context_id(self) -> None:
|
||||
"""Use the _current_context to set _current_context_id."""
|
||||
context = self._current_context or ""
|
||||
context = self._current_context or ''
|
||||
context_id = self._context_id(context)
|
||||
if context_id is not None:
|
||||
self._current_context_id = context_id
|
||||
else:
|
||||
with self._connect() as con:
|
||||
self._current_context_id = con.execute_for_rowid(
|
||||
"insert into context (context) values (?)",
|
||||
'insert into context (context) values (?)',
|
||||
(context,),
|
||||
)
|
||||
|
||||
|
|
@ -484,13 +493,15 @@ class CoverageData:
|
|||
{ filename: { line1, line2, ... }, ...}
|
||||
|
||||
"""
|
||||
if self._debug.should("dataop"):
|
||||
self._debug.write("Adding lines: %d files, %d lines total" % (
|
||||
len(line_data), sum(len(lines) for lines in line_data.values()),
|
||||
))
|
||||
if self._debug.should("dataop2"):
|
||||
if self._debug.should('dataop'):
|
||||
self._debug.write(
|
||||
'Adding lines: %d files, %d lines total' % (
|
||||
len(line_data), sum(len(lines) for lines in line_data.values()),
|
||||
),
|
||||
)
|
||||
if self._debug.should('dataop2'):
|
||||
for filename, linenos in sorted(line_data.items()):
|
||||
self._debug.write(f" {filename}: {linenos}")
|
||||
self._debug.write(f' {filename}: {linenos}')
|
||||
self._start_using()
|
||||
self._choose_lines_or_arcs(lines=True)
|
||||
if not line_data:
|
||||
|
|
@ -500,15 +511,15 @@ class CoverageData:
|
|||
for filename, linenos in line_data.items():
|
||||
linemap = nums_to_numbits(linenos)
|
||||
file_id = self._file_id(filename, add=True)
|
||||
query = "select numbits from line_bits where file_id = ? and context_id = ?"
|
||||
query = 'select numbits from line_bits where file_id = ? and context_id = ?'
|
||||
with con.execute(query, (file_id, self._current_context_id)) as cur:
|
||||
existing = list(cur)
|
||||
if existing:
|
||||
linemap = numbits_union(linemap, existing[0][0])
|
||||
|
||||
con.execute_void(
|
||||
"insert or replace into line_bits " +
|
||||
" (file_id, context_id, numbits) values (?, ?, ?)",
|
||||
'insert or replace into line_bits ' +
|
||||
' (file_id, context_id, numbits) values (?, ?, ?)',
|
||||
(file_id, self._current_context_id, linemap),
|
||||
)
|
||||
|
||||
|
|
@ -522,13 +533,15 @@ class CoverageData:
|
|||
{ filename: { (l1,l2), (l1,l2), ... }, ...}
|
||||
|
||||
"""
|
||||
if self._debug.should("dataop"):
|
||||
self._debug.write("Adding arcs: %d files, %d arcs total" % (
|
||||
len(arc_data), sum(len(arcs) for arcs in arc_data.values()),
|
||||
))
|
||||
if self._debug.should("dataop2"):
|
||||
if self._debug.should('dataop'):
|
||||
self._debug.write(
|
||||
'Adding arcs: %d files, %d arcs total' % (
|
||||
len(arc_data), sum(len(arcs) for arcs in arc_data.values()),
|
||||
),
|
||||
)
|
||||
if self._debug.should('dataop2'):
|
||||
for filename, arcs in sorted(arc_data.items()):
|
||||
self._debug.write(f" {filename}: {arcs}")
|
||||
self._debug.write(f' {filename}: {arcs}')
|
||||
self._start_using()
|
||||
self._choose_lines_or_arcs(arcs=True)
|
||||
if not arc_data:
|
||||
|
|
@ -541,8 +554,8 @@ class CoverageData:
|
|||
file_id = self._file_id(filename, add=True)
|
||||
data = [(file_id, self._current_context_id, fromno, tono) for fromno, tono in arcs]
|
||||
con.executemany_void(
|
||||
"insert or ignore into arc " +
|
||||
"(file_id, context_id, fromno, tono) values (?, ?, ?, ?)",
|
||||
'insert or ignore into arc ' +
|
||||
'(file_id, context_id, fromno, tono) values (?, ?, ?, ?)',
|
||||
data,
|
||||
)
|
||||
|
||||
|
|
@ -551,11 +564,11 @@ class CoverageData:
|
|||
assert lines or arcs
|
||||
assert not (lines and arcs)
|
||||
if lines and self._has_arcs:
|
||||
if self._debug.should("dataop"):
|
||||
if self._debug.should('dataop'):
|
||||
self._debug.write("Error: Can't add line measurements to existing branch data")
|
||||
raise DataError("Can't add line measurements to existing branch data")
|
||||
if arcs and self._has_lines:
|
||||
if self._debug.should("dataop"):
|
||||
if self._debug.should('dataop'):
|
||||
self._debug.write("Error: Can't add branch measurements to existing line data")
|
||||
raise DataError("Can't add branch measurements to existing line data")
|
||||
if not self._has_arcs and not self._has_lines:
|
||||
|
|
@ -563,8 +576,8 @@ class CoverageData:
|
|||
self._has_arcs = arcs
|
||||
with self._connect() as con:
|
||||
con.execute_void(
|
||||
"insert or ignore into meta (key, value) values (?, ?)",
|
||||
("has_arcs", str(int(arcs))),
|
||||
'insert or ignore into meta (key, value) values (?, ?)',
|
||||
('has_arcs', str(int(arcs))),
|
||||
)
|
||||
|
||||
@_locked
|
||||
|
|
@ -574,8 +587,8 @@ class CoverageData:
|
|||
`file_tracers` is { filename: plugin_name, ... }
|
||||
|
||||
"""
|
||||
if self._debug.should("dataop"):
|
||||
self._debug.write("Adding file tracers: %d files" % (len(file_tracers),))
|
||||
if self._debug.should('dataop'):
|
||||
self._debug.write('Adding file tracers: %d files' % (len(file_tracers),))
|
||||
if not file_tracers:
|
||||
return
|
||||
self._start_using()
|
||||
|
|
@ -592,11 +605,11 @@ class CoverageData:
|
|||
)
|
||||
elif plugin_name:
|
||||
con.execute_void(
|
||||
"insert into tracer (file_id, tracer) values (?, ?)",
|
||||
'insert into tracer (file_id, tracer) values (?, ?)',
|
||||
(file_id, plugin_name),
|
||||
)
|
||||
|
||||
def touch_file(self, filename: str, plugin_name: str = "") -> None:
|
||||
def touch_file(self, filename: str, plugin_name: str = '') -> None:
|
||||
"""Ensure that `filename` appears in the data, empty if needed.
|
||||
|
||||
`plugin_name` is the name of the plugin responsible for this file.
|
||||
|
|
@ -610,10 +623,10 @@ class CoverageData:
|
|||
`plugin_name` is the name of the plugin responsible for these files.
|
||||
It is used to associate the right filereporter, etc.
|
||||
"""
|
||||
if self._debug.should("dataop"):
|
||||
self._debug.write(f"Touching {filenames!r}")
|
||||
if self._debug.should('dataop'):
|
||||
self._debug.write(f'Touching {filenames!r}')
|
||||
self._start_using()
|
||||
with self._connect(): # Use this to get one transaction.
|
||||
with self._connect(): # Use this to get one transaction.
|
||||
if not self._has_arcs and not self._has_lines:
|
||||
raise DataError("Can't touch files in an empty CoverageData")
|
||||
|
||||
|
|
@ -629,15 +642,15 @@ class CoverageData:
|
|||
.. versionadded:: 7.2
|
||||
|
||||
"""
|
||||
if self._debug.should("dataop"):
|
||||
self._debug.write(f"Purging data for {filenames!r}")
|
||||
if self._debug.should('dataop'):
|
||||
self._debug.write(f'Purging data for {filenames!r}')
|
||||
self._start_using()
|
||||
with self._connect() as con:
|
||||
|
||||
if self._has_lines:
|
||||
sql = "delete from line_bits where file_id=?"
|
||||
sql = 'delete from line_bits where file_id=?'
|
||||
elif self._has_arcs:
|
||||
sql = "delete from arc where file_id=?"
|
||||
sql = 'delete from arc where file_id=?'
|
||||
else:
|
||||
raise DataError("Can't purge files in an empty CoverageData")
|
||||
|
||||
|
|
@ -655,10 +668,12 @@ class CoverageData:
|
|||
only when called directly from the test suite.
|
||||
|
||||
"""
|
||||
if self._debug.should("dataop"):
|
||||
self._debug.write("Updating with data from {!r}".format(
|
||||
getattr(other_data, "_filename", "???"),
|
||||
))
|
||||
if self._debug.should('dataop'):
|
||||
self._debug.write(
|
||||
'Updating with data from {!r}'.format(
|
||||
getattr(other_data, '_filename', '???'),
|
||||
),
|
||||
)
|
||||
if self._has_lines and other_data._has_arcs:
|
||||
raise DataError("Can't combine arc data with line data")
|
||||
if self._has_arcs and other_data._has_lines:
|
||||
|
|
@ -673,19 +688,19 @@ class CoverageData:
|
|||
other_data.read()
|
||||
with other_data._connect() as con:
|
||||
# Get files data.
|
||||
with con.execute("select path from file") as cur:
|
||||
with con.execute('select path from file') as cur:
|
||||
files = {path: aliases.map(path) for (path,) in cur}
|
||||
|
||||
# Get contexts data.
|
||||
with con.execute("select context from context") as cur:
|
||||
with con.execute('select context from context') as cur:
|
||||
contexts = [context for (context,) in cur]
|
||||
|
||||
# Get arc data.
|
||||
with con.execute(
|
||||
"select file.path, context.context, arc.fromno, arc.tono " +
|
||||
"from arc " +
|
||||
"inner join file on file.id = arc.file_id " +
|
||||
"inner join context on context.id = arc.context_id",
|
||||
'select file.path, context.context, arc.fromno, arc.tono ' +
|
||||
'from arc ' +
|
||||
'inner join file on file.id = arc.file_id ' +
|
||||
'inner join context on context.id = arc.context_id',
|
||||
) as cur:
|
||||
arcs = [
|
||||
(files[path], context, fromno, tono)
|
||||
|
|
@ -694,10 +709,10 @@ class CoverageData:
|
|||
|
||||
# Get line data.
|
||||
with con.execute(
|
||||
"select file.path, context.context, line_bits.numbits " +
|
||||
"from line_bits " +
|
||||
"inner join file on file.id = line_bits.file_id " +
|
||||
"inner join context on context.id = line_bits.context_id",
|
||||
'select file.path, context.context, line_bits.numbits ' +
|
||||
'from line_bits ' +
|
||||
'inner join file on file.id = line_bits.file_id ' +
|
||||
'inner join context on context.id = line_bits.context_id',
|
||||
) as cur:
|
||||
lines: dict[tuple[str, str], bytes] = {}
|
||||
for path, context, numbits in cur:
|
||||
|
|
@ -708,25 +723,25 @@ class CoverageData:
|
|||
|
||||
# Get tracer data.
|
||||
with con.execute(
|
||||
"select file.path, tracer " +
|
||||
"from tracer " +
|
||||
"inner join file on file.id = tracer.file_id",
|
||||
'select file.path, tracer ' +
|
||||
'from tracer ' +
|
||||
'inner join file on file.id = tracer.file_id',
|
||||
) as cur:
|
||||
tracers = {files[path]: tracer for (path, tracer) in cur}
|
||||
|
||||
with self._connect() as con:
|
||||
assert con.con is not None
|
||||
con.con.isolation_level = "IMMEDIATE"
|
||||
con.con.isolation_level = 'IMMEDIATE'
|
||||
|
||||
# Get all tracers in the DB. Files not in the tracers are assumed
|
||||
# to have an empty string tracer. Since Sqlite does not support
|
||||
# full outer joins, we have to make two queries to fill the
|
||||
# dictionary.
|
||||
with con.execute("select path from file") as cur:
|
||||
this_tracers = {path: "" for path, in cur}
|
||||
with con.execute('select path from file') as cur:
|
||||
this_tracers = {path: '' for path, in cur}
|
||||
with con.execute(
|
||||
"select file.path, tracer from tracer " +
|
||||
"inner join file on file.id = tracer.file_id",
|
||||
'select file.path, tracer from tracer ' +
|
||||
'inner join file on file.id = tracer.file_id',
|
||||
) as cur:
|
||||
this_tracers.update({
|
||||
aliases.map(path): tracer
|
||||
|
|
@ -735,17 +750,17 @@ class CoverageData:
|
|||
|
||||
# Create all file and context rows in the DB.
|
||||
con.executemany_void(
|
||||
"insert or ignore into file (path) values (?)",
|
||||
'insert or ignore into file (path) values (?)',
|
||||
((file,) for file in files.values()),
|
||||
)
|
||||
with con.execute("select id, path from file") as cur:
|
||||
with con.execute('select id, path from file') as cur:
|
||||
file_ids = {path: id for id, path in cur}
|
||||
self._file_map.update(file_ids)
|
||||
con.executemany_void(
|
||||
"insert or ignore into context (context) values (?)",
|
||||
'insert or ignore into context (context) values (?)',
|
||||
((context,) for context in contexts),
|
||||
)
|
||||
with con.execute("select id, context from context") as cur:
|
||||
with con.execute('select id, context from context') as cur:
|
||||
context_ids = {context: id for id, context in cur}
|
||||
|
||||
# Prepare tracers and fail, if a conflict is found.
|
||||
|
|
@ -754,7 +769,7 @@ class CoverageData:
|
|||
tracer_map = {}
|
||||
for path in files.values():
|
||||
this_tracer = this_tracers.get(path)
|
||||
other_tracer = tracers.get(path, "")
|
||||
other_tracer = tracers.get(path, '')
|
||||
# If there is no tracer, there is always the None tracer.
|
||||
if this_tracer is not None and this_tracer != other_tracer:
|
||||
raise DataError(
|
||||
|
|
@ -774,10 +789,10 @@ class CoverageData:
|
|||
|
||||
# Get line data.
|
||||
with con.execute(
|
||||
"select file.path, context.context, line_bits.numbits " +
|
||||
"from line_bits " +
|
||||
"inner join file on file.id = line_bits.file_id " +
|
||||
"inner join context on context.id = line_bits.context_id",
|
||||
'select file.path, context.context, line_bits.numbits ' +
|
||||
'from line_bits ' +
|
||||
'inner join file on file.id = line_bits.file_id ' +
|
||||
'inner join context on context.id = line_bits.context_id',
|
||||
) as cur:
|
||||
for path, context, numbits in cur:
|
||||
key = (aliases.map(path), context)
|
||||
|
|
@ -790,24 +805,24 @@ class CoverageData:
|
|||
|
||||
# Write the combined data.
|
||||
con.executemany_void(
|
||||
"insert or ignore into arc " +
|
||||
"(file_id, context_id, fromno, tono) values (?, ?, ?, ?)",
|
||||
'insert or ignore into arc ' +
|
||||
'(file_id, context_id, fromno, tono) values (?, ?, ?, ?)',
|
||||
arc_rows,
|
||||
)
|
||||
|
||||
if lines:
|
||||
self._choose_lines_or_arcs(lines=True)
|
||||
con.execute_void("delete from line_bits")
|
||||
con.execute_void('delete from line_bits')
|
||||
con.executemany_void(
|
||||
"insert into line_bits " +
|
||||
"(file_id, context_id, numbits) values (?, ?, ?)",
|
||||
'insert into line_bits ' +
|
||||
'(file_id, context_id, numbits) values (?, ?, ?)',
|
||||
[
|
||||
(file_ids[file], context_ids[context], numbits)
|
||||
for (file, context), numbits in lines.items()
|
||||
],
|
||||
)
|
||||
con.executemany_void(
|
||||
"insert or ignore into tracer (file_id, tracer) values (?, ?)",
|
||||
'insert or ignore into tracer (file_id, tracer) values (?, ?)',
|
||||
((file_ids[filename], tracer) for filename, tracer in tracer_map.items()),
|
||||
)
|
||||
|
||||
|
|
@ -826,16 +841,16 @@ class CoverageData:
|
|||
self._reset()
|
||||
if self._no_disk:
|
||||
return
|
||||
if self._debug.should("dataio"):
|
||||
self._debug.write(f"Erasing data file {self._filename!r}")
|
||||
if self._debug.should('dataio'):
|
||||
self._debug.write(f'Erasing data file {self._filename!r}')
|
||||
file_be_gone(self._filename)
|
||||
if parallel:
|
||||
data_dir, local = os.path.split(self._filename)
|
||||
local_abs_path = os.path.join(os.path.abspath(data_dir), local)
|
||||
pattern = glob.escape(local_abs_path) + ".*"
|
||||
pattern = glob.escape(local_abs_path) + '.*'
|
||||
for filename in glob.glob(pattern):
|
||||
if self._debug.should("dataio"):
|
||||
self._debug.write(f"Erasing parallel data file {filename!r}")
|
||||
if self._debug.should('dataio'):
|
||||
self._debug.write(f'Erasing parallel data file {filename!r}')
|
||||
file_be_gone(filename)
|
||||
|
||||
def read(self) -> None:
|
||||
|
|
@ -880,7 +895,7 @@ class CoverageData:
|
|||
"""
|
||||
self._start_using()
|
||||
with self._connect() as con:
|
||||
with con.execute("select distinct(context) from context") as cur:
|
||||
with con.execute('select distinct(context) from context') as cur:
|
||||
contexts = {row[0] for row in cur}
|
||||
return contexts
|
||||
|
||||
|
|
@ -897,10 +912,10 @@ class CoverageData:
|
|||
file_id = self._file_id(filename)
|
||||
if file_id is None:
|
||||
return None
|
||||
row = con.execute_one("select tracer from tracer where file_id = ?", (file_id,))
|
||||
row = con.execute_one('select tracer from tracer where file_id = ?', (file_id,))
|
||||
if row is not None:
|
||||
return row[0] or ""
|
||||
return "" # File was measured, but no tracer associated.
|
||||
return row[0] or ''
|
||||
return '' # File was measured, but no tracer associated.
|
||||
|
||||
def set_query_context(self, context: str) -> None:
|
||||
"""Set a context for subsequent querying.
|
||||
|
|
@ -915,7 +930,7 @@ class CoverageData:
|
|||
"""
|
||||
self._start_using()
|
||||
with self._connect() as con:
|
||||
with con.execute("select id from context where context = ?", (context,)) as cur:
|
||||
with con.execute('select id from context where context = ?', (context,)) as cur:
|
||||
self._query_context_ids = [row[0] for row in cur.fetchall()]
|
||||
|
||||
def set_query_contexts(self, contexts: Sequence[str] | None) -> None:
|
||||
|
|
@ -933,8 +948,8 @@ class CoverageData:
|
|||
self._start_using()
|
||||
if contexts:
|
||||
with self._connect() as con:
|
||||
context_clause = " or ".join(["context regexp ?"] * len(contexts))
|
||||
with con.execute("select id from context where " + context_clause, contexts) as cur:
|
||||
context_clause = ' or '.join(['context regexp ?'] * len(contexts))
|
||||
with con.execute('select id from context where ' + context_clause, contexts) as cur:
|
||||
self._query_context_ids = [row[0] for row in cur.fetchall()]
|
||||
else:
|
||||
self._query_context_ids = None
|
||||
|
|
@ -961,11 +976,11 @@ class CoverageData:
|
|||
if file_id is None:
|
||||
return None
|
||||
else:
|
||||
query = "select numbits from line_bits where file_id = ?"
|
||||
query = 'select numbits from line_bits where file_id = ?'
|
||||
data = [file_id]
|
||||
if self._query_context_ids is not None:
|
||||
ids_array = ", ".join("?" * len(self._query_context_ids))
|
||||
query += " and context_id in (" + ids_array + ")"
|
||||
ids_array = ', '.join('?' * len(self._query_context_ids))
|
||||
query += ' and context_id in (' + ids_array + ')'
|
||||
data += self._query_context_ids
|
||||
with con.execute(query, data) as cur:
|
||||
bitmaps = list(cur)
|
||||
|
|
@ -997,11 +1012,11 @@ class CoverageData:
|
|||
if file_id is None:
|
||||
return None
|
||||
else:
|
||||
query = "select distinct fromno, tono from arc where file_id = ?"
|
||||
query = 'select distinct fromno, tono from arc where file_id = ?'
|
||||
data = [file_id]
|
||||
if self._query_context_ids is not None:
|
||||
ids_array = ", ".join("?" * len(self._query_context_ids))
|
||||
query += " and context_id in (" + ids_array + ")"
|
||||
ids_array = ', '.join('?' * len(self._query_context_ids))
|
||||
query += ' and context_id in (' + ids_array + ')'
|
||||
data += self._query_context_ids
|
||||
with con.execute(query, data) as cur:
|
||||
return list(cur)
|
||||
|
|
@ -1024,14 +1039,14 @@ class CoverageData:
|
|||
lineno_contexts_map = collections.defaultdict(set)
|
||||
if self.has_arcs():
|
||||
query = (
|
||||
"select arc.fromno, arc.tono, context.context " +
|
||||
"from arc, context " +
|
||||
"where arc.file_id = ? and arc.context_id = context.id"
|
||||
'select arc.fromno, arc.tono, context.context ' +
|
||||
'from arc, context ' +
|
||||
'where arc.file_id = ? and arc.context_id = context.id'
|
||||
)
|
||||
data = [file_id]
|
||||
if self._query_context_ids is not None:
|
||||
ids_array = ", ".join("?" * len(self._query_context_ids))
|
||||
query += " and arc.context_id in (" + ids_array + ")"
|
||||
ids_array = ', '.join('?' * len(self._query_context_ids))
|
||||
query += ' and arc.context_id in (' + ids_array + ')'
|
||||
data += self._query_context_ids
|
||||
with con.execute(query, data) as cur:
|
||||
for fromno, tono, context in cur:
|
||||
|
|
@ -1041,14 +1056,14 @@ class CoverageData:
|
|||
lineno_contexts_map[tono].add(context)
|
||||
else:
|
||||
query = (
|
||||
"select l.numbits, c.context from line_bits l, context c " +
|
||||
"where l.context_id = c.id " +
|
||||
"and file_id = ?"
|
||||
'select l.numbits, c.context from line_bits l, context c ' +
|
||||
'where l.context_id = c.id ' +
|
||||
'and file_id = ?'
|
||||
)
|
||||
data = [file_id]
|
||||
if self._query_context_ids is not None:
|
||||
ids_array = ", ".join("?" * len(self._query_context_ids))
|
||||
query += " and l.context_id in (" + ids_array + ")"
|
||||
ids_array = ', '.join('?' * len(self._query_context_ids))
|
||||
query += ' and l.context_id in (' + ids_array + ')'
|
||||
data += self._query_context_ids
|
||||
with con.execute(query, data) as cur:
|
||||
for numbits, context in cur:
|
||||
|
|
@ -1064,17 +1079,17 @@ class CoverageData:
|
|||
Returns a list of (key, value) pairs.
|
||||
|
||||
"""
|
||||
with SqliteDb(":memory:", debug=NoDebugging()) as db:
|
||||
with db.execute("pragma temp_store") as cur:
|
||||
with SqliteDb(':memory:', debug=NoDebugging()) as db:
|
||||
with db.execute('pragma temp_store') as cur:
|
||||
temp_store = [row[0] for row in cur]
|
||||
with db.execute("pragma compile_options") as cur:
|
||||
with db.execute('pragma compile_options') as cur:
|
||||
copts = [row[0] for row in cur]
|
||||
copts = textwrap.wrap(", ".join(copts), width=75)
|
||||
copts = textwrap.wrap(', '.join(copts), width=75)
|
||||
|
||||
return [
|
||||
("sqlite3_sqlite_version", sqlite3.sqlite_version),
|
||||
("sqlite3_temp_store", temp_store),
|
||||
("sqlite3_compile_options", copts),
|
||||
('sqlite3_sqlite_version', sqlite3.sqlite_version),
|
||||
('sqlite3_temp_store', temp_store),
|
||||
('sqlite3_compile_options', copts),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -1095,8 +1110,8 @@ def filename_suffix(suffix: str | bool | None) -> str | None:
|
|||
# if the process forks.
|
||||
die = random.Random(os.urandom(8))
|
||||
letters = string.ascii_uppercase + string.ascii_lowercase
|
||||
rolls = "".join(die.choice(letters) for _ in range(6))
|
||||
suffix = f"{socket.gethostname()}.{os.getpid()}.X{rolls}x"
|
||||
rolls = ''.join(die.choice(letters) for _ in range(6))
|
||||
suffix = f'{socket.gethostname()}.{os.getpid()}.X{rolls}x'
|
||||
elif suffix is False:
|
||||
suffix = None
|
||||
return suffix
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""SQLite abstraction for coverage.py"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import re
|
||||
import sqlite3
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import Tuple
|
||||
|
||||
from typing import cast, Any, Iterable, Iterator, Tuple
|
||||
|
||||
from coverage.debug import auto_repr, clipped_repr, exc_one_line
|
||||
from coverage.debug import auto_repr
|
||||
from coverage.debug import clipped_repr
|
||||
from coverage.debug import exc_one_line
|
||||
from coverage.exceptions import DataError
|
||||
from coverage.types import TDebugCtl
|
||||
|
||||
|
|
@ -28,6 +31,7 @@ class SqliteDb:
|
|||
etc(a, b)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, filename: str, debug: TDebugCtl) -> None:
|
||||
self.debug = debug
|
||||
self.filename = filename
|
||||
|
|
@ -46,40 +50,40 @@ class SqliteDb:
|
|||
# effectively causing a nested context. However, given the idempotent
|
||||
# nature of the tracer operations, sharing a connection among threads
|
||||
# is not a problem.
|
||||
if self.debug.should("sql"):
|
||||
self.debug.write(f"Connecting to {self.filename!r}")
|
||||
if self.debug.should('sql'):
|
||||
self.debug.write(f'Connecting to {self.filename!r}')
|
||||
try:
|
||||
self.con = sqlite3.connect(self.filename, check_same_thread=False)
|
||||
except sqlite3.Error as exc:
|
||||
raise DataError(f"Couldn't use data file {self.filename!r}: {exc}") from exc
|
||||
|
||||
if self.debug.should("sql"):
|
||||
self.debug.write(f"Connected to {self.filename!r} as {self.con!r}")
|
||||
if self.debug.should('sql'):
|
||||
self.debug.write(f'Connected to {self.filename!r} as {self.con!r}')
|
||||
|
||||
self.con.create_function("REGEXP", 2, lambda txt, pat: re.search(txt, pat) is not None)
|
||||
self.con.create_function('REGEXP', 2, lambda txt, pat: re.search(txt, pat) is not None)
|
||||
|
||||
# Turning off journal_mode can speed up writing. It can't always be
|
||||
# disabled, so we have to be prepared for *-journal files elsewhere.
|
||||
# In Python 3.12+, we can change the config to allow journal_mode=off.
|
||||
if hasattr(sqlite3, "SQLITE_DBCONFIG_DEFENSIVE"):
|
||||
if hasattr(sqlite3, 'SQLITE_DBCONFIG_DEFENSIVE'):
|
||||
# Turn off defensive mode, so that journal_mode=off can succeed.
|
||||
self.con.setconfig( # type: ignore[attr-defined, unused-ignore]
|
||||
sqlite3.SQLITE_DBCONFIG_DEFENSIVE, False,
|
||||
)
|
||||
|
||||
# This pragma makes writing faster. It disables rollbacks, but we never need them.
|
||||
self.execute_void("pragma journal_mode=off")
|
||||
self.execute_void('pragma journal_mode=off')
|
||||
|
||||
# This pragma makes writing faster. It can fail in unusual situations
|
||||
# (https://github.com/nedbat/coveragepy/issues/1646), so use fail_ok=True
|
||||
# to keep things going.
|
||||
self.execute_void("pragma synchronous=off", fail_ok=True)
|
||||
self.execute_void('pragma synchronous=off', fail_ok=True)
|
||||
|
||||
def close(self) -> None:
|
||||
"""If needed, close the connection."""
|
||||
if self.con is not None and self.filename != ":memory:":
|
||||
if self.debug.should("sql"):
|
||||
self.debug.write(f"Closing {self.con!r} on {self.filename!r}")
|
||||
if self.con is not None and self.filename != ':memory:':
|
||||
if self.debug.should('sql'):
|
||||
self.debug.write(f'Closing {self.con!r} on {self.filename!r}')
|
||||
self.con.close()
|
||||
self.con = None
|
||||
|
||||
|
|
@ -99,15 +103,15 @@ class SqliteDb:
|
|||
self.con.__exit__(exc_type, exc_value, traceback)
|
||||
self.close()
|
||||
except Exception as exc:
|
||||
if self.debug.should("sql"):
|
||||
self.debug.write(f"EXCEPTION from __exit__: {exc_one_line(exc)}")
|
||||
if self.debug.should('sql'):
|
||||
self.debug.write(f'EXCEPTION from __exit__: {exc_one_line(exc)}')
|
||||
raise DataError(f"Couldn't end data file {self.filename!r}: {exc}") from exc
|
||||
|
||||
def _execute(self, sql: str, parameters: Iterable[Any]) -> sqlite3.Cursor:
|
||||
"""Same as :meth:`python:sqlite3.Connection.execute`."""
|
||||
if self.debug.should("sql"):
|
||||
tail = f" with {parameters!r}" if parameters else ""
|
||||
self.debug.write(f"Executing {sql!r}{tail}")
|
||||
if self.debug.should('sql'):
|
||||
tail = f' with {parameters!r}' if parameters else ''
|
||||
self.debug.write(f'Executing {sql!r}{tail}')
|
||||
try:
|
||||
assert self.con is not None
|
||||
try:
|
||||
|
|
@ -119,21 +123,21 @@ class SqliteDb:
|
|||
return self.con.execute(sql, parameters) # type: ignore[arg-type]
|
||||
except sqlite3.Error as exc:
|
||||
msg = str(exc)
|
||||
if self.filename != ":memory:":
|
||||
if self.filename != ':memory:':
|
||||
try:
|
||||
# `execute` is the first thing we do with the database, so try
|
||||
# hard to provide useful hints if something goes wrong now.
|
||||
with open(self.filename, "rb") as bad_file:
|
||||
cov4_sig = b"!coverage.py: This is a private format"
|
||||
with open(self.filename, 'rb') as bad_file:
|
||||
cov4_sig = b'!coverage.py: This is a private format'
|
||||
if bad_file.read(len(cov4_sig)) == cov4_sig:
|
||||
msg = (
|
||||
"Looks like a coverage 4.x data file. " +
|
||||
"Are you mixing versions of coverage?"
|
||||
'Looks like a coverage 4.x data file. ' +
|
||||
'Are you mixing versions of coverage?'
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
if self.debug.should("sql"):
|
||||
self.debug.write(f"EXCEPTION from execute: {exc_one_line(exc)}")
|
||||
if self.debug.should('sql'):
|
||||
self.debug.write(f'EXCEPTION from execute: {exc_one_line(exc)}')
|
||||
raise DataError(f"Couldn't use data file {self.filename!r}: {msg}") from exc
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
|
@ -170,8 +174,8 @@ class SqliteDb:
|
|||
with self.execute(sql, parameters) as cur:
|
||||
assert cur.lastrowid is not None
|
||||
rowid: int = cur.lastrowid
|
||||
if self.debug.should("sqldata"):
|
||||
self.debug.write(f"Row id result: {rowid!r}")
|
||||
if self.debug.should('sqldata'):
|
||||
self.debug.write(f'Row id result: {rowid!r}')
|
||||
return rowid
|
||||
|
||||
def execute_one(self, sql: str, parameters: Iterable[Any] = ()) -> tuple[Any, ...] | None:
|
||||
|
|
@ -194,12 +198,12 @@ class SqliteDb:
|
|||
|
||||
def _executemany(self, sql: str, data: list[Any]) -> sqlite3.Cursor:
|
||||
"""Same as :meth:`python:sqlite3.Connection.executemany`."""
|
||||
if self.debug.should("sql"):
|
||||
final = ":" if self.debug.should("sqldata") else ""
|
||||
self.debug.write(f"Executing many {sql!r} with {len(data)} rows{final}")
|
||||
if self.debug.should("sqldata"):
|
||||
if self.debug.should('sql'):
|
||||
final = ':' if self.debug.should('sqldata') else ''
|
||||
self.debug.write(f'Executing many {sql!r} with {len(data)} rows{final}')
|
||||
if self.debug.should('sqldata'):
|
||||
for i, row in enumerate(data):
|
||||
self.debug.write(f"{i:4d}: {row!r}")
|
||||
self.debug.write(f'{i:4d}: {row!r}')
|
||||
assert self.con is not None
|
||||
try:
|
||||
return self.con.executemany(sql, data)
|
||||
|
|
@ -217,14 +221,16 @@ class SqliteDb:
|
|||
|
||||
def executescript(self, script: str) -> None:
|
||||
"""Same as :meth:`python:sqlite3.Connection.executescript`."""
|
||||
if self.debug.should("sql"):
|
||||
self.debug.write("Executing script with {} chars: {}".format(
|
||||
len(script), clipped_repr(script, 100),
|
||||
))
|
||||
if self.debug.should('sql'):
|
||||
self.debug.write(
|
||||
'Executing script with {} chars: {}'.format(
|
||||
len(script), clipped_repr(script, 100),
|
||||
),
|
||||
)
|
||||
assert self.con is not None
|
||||
self.con.executescript(script).close()
|
||||
|
||||
def dump(self) -> str:
|
||||
"""Return a multi-line string, the SQL dump of the database."""
|
||||
assert self.con is not None
|
||||
return "\n".join(self.con.iterdump())
|
||||
return '\n'.join(self.con.iterdump())
|
||||
|
|
|
|||
|
|
@ -1,39 +1,33 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""Callback functions and support for sys.monitoring data collection."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from dataclasses import dataclass
|
||||
from types import CodeType, FrameType
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Set,
|
||||
TYPE_CHECKING,
|
||||
cast,
|
||||
)
|
||||
from types import CodeType
|
||||
from types import FrameType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Set
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from coverage.debug import short_filename, short_stack
|
||||
from coverage.types import (
|
||||
AnyCallable,
|
||||
TArc,
|
||||
TFileDisposition,
|
||||
TLineNo,
|
||||
TTraceData,
|
||||
TTraceFileData,
|
||||
TracerCore,
|
||||
TWarnFn,
|
||||
)
|
||||
from coverage.debug import short_filename
|
||||
from coverage.debug import short_stack
|
||||
from coverage.types import AnyCallable
|
||||
from coverage.types import TArc
|
||||
from coverage.types import TFileDisposition
|
||||
from coverage.types import TLineNo
|
||||
from coverage.types import TracerCore
|
||||
from coverage.types import TTraceData
|
||||
from coverage.types import TTraceFileData
|
||||
from coverage.types import TWarnFn
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
|
|
@ -41,7 +35,7 @@ LOG = False
|
|||
|
||||
# This module will be imported in all versions of Python, but only used in 3.12+
|
||||
# It will be type-checked for 3.12, but not for earlier versions.
|
||||
sys_monitoring = getattr(sys, "monitoring", None)
|
||||
sys_monitoring = getattr(sys, 'monitoring', None)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert sys_monitoring is not None
|
||||
|
|
@ -61,12 +55,12 @@ if LOG: # pragma: debugging
|
|||
|
||||
def __getattr__(self, name: str) -> Callable[..., Any]:
|
||||
def _wrapped(*args: Any, **kwargs: Any) -> Any:
|
||||
log(f"{self.namespace}.{name}{args}{kwargs}")
|
||||
log(f'{self.namespace}.{name}{args}{kwargs}')
|
||||
return getattr(self.wrapped, name)(*args, **kwargs)
|
||||
|
||||
return _wrapped
|
||||
|
||||
sys_monitoring = LoggingWrapper(sys_monitoring, "sys.monitoring")
|
||||
sys_monitoring = LoggingWrapper(sys_monitoring, 'sys.monitoring')
|
||||
assert sys_monitoring is not None
|
||||
|
||||
short_stack = functools.partial(
|
||||
|
|
@ -80,28 +74,28 @@ if LOG: # pragma: debugging
|
|||
# Make a shorter number more likely to be unique.
|
||||
pid = os.getpid()
|
||||
tid = cast(int, threading.current_thread().ident)
|
||||
tslug = f"{(pid * tid) % 9_999_991:07d}"
|
||||
tslug = f'{(pid * tid) % 9_999_991:07d}'
|
||||
if tid not in seen_threads:
|
||||
seen_threads.add(tid)
|
||||
log(f"New thread {tid} {tslug}:\n{short_stack()}")
|
||||
log(f'New thread {tid} {tslug}:\n{short_stack()}')
|
||||
# log_seq = int(os.getenv("PANSEQ", "0"))
|
||||
# root = f"/tmp/pan.{log_seq:03d}"
|
||||
for filename in [
|
||||
"/tmp/foo.out",
|
||||
'/tmp/foo.out',
|
||||
# f"{root}.out",
|
||||
# f"{root}-{pid}.out",
|
||||
# f"{root}-{pid}-{tslug}.out",
|
||||
]:
|
||||
with open(filename, "a") as f:
|
||||
print(f"{pid}:{tslug}: {msg}", file=f, flush=True)
|
||||
with open(filename, 'a') as f:
|
||||
print(f'{pid}:{tslug}: {msg}', file=f, flush=True)
|
||||
|
||||
def arg_repr(arg: Any) -> str:
|
||||
"""Make a customized repr for logged values."""
|
||||
if isinstance(arg, CodeType):
|
||||
return (
|
||||
f"<code @{id(arg):#x}"
|
||||
+ f" name={arg.co_name},"
|
||||
+ f" file={short_filename(arg.co_filename)!r}#{arg.co_firstlineno}>"
|
||||
f'<code @{id(arg):#x}' +
|
||||
f' name={arg.co_name},' +
|
||||
f' file={short_filename(arg.co_filename)!r}#{arg.co_firstlineno}>'
|
||||
)
|
||||
return repr(arg)
|
||||
|
||||
|
|
@ -117,20 +111,20 @@ if LOG: # pragma: debugging
|
|||
for name, arg in zip(names, args):
|
||||
if name is None:
|
||||
continue
|
||||
args_reprs.append(f"{name}={arg_repr(arg)}")
|
||||
args_reprs.append(f'{name}={arg_repr(arg)}')
|
||||
log(f"{id(self):#x}:{method.__name__}({', '.join(args_reprs)})")
|
||||
ret = method(self, *args)
|
||||
# log(f" end {id(self):#x}:{method.__name__}({', '.join(args_reprs)})")
|
||||
return ret
|
||||
except Exception as exc:
|
||||
log(f"!!{exc.__class__.__name__}: {exc}")
|
||||
log("".join(traceback.format_exception(exc))) # pylint: disable=[no-value-for-parameter]
|
||||
log(f'!!{exc.__class__.__name__}: {exc}')
|
||||
log(''.join(traceback.format_exception(exc))) # pylint: disable=[no-value-for-parameter]
|
||||
try:
|
||||
assert sys_monitoring is not None
|
||||
sys_monitoring.set_events(sys.monitoring.COVERAGE_ID, 0)
|
||||
except ValueError:
|
||||
# We might have already shut off monitoring.
|
||||
log("oops, shutting off events with disabled tool id")
|
||||
log('oops, shutting off events with disabled tool id')
|
||||
raise
|
||||
|
||||
return _wrapped
|
||||
|
|
@ -202,7 +196,7 @@ class SysMonitor(TracerCore):
|
|||
self.sysmon_on = False
|
||||
|
||||
self.stats = {
|
||||
"starts": 0,
|
||||
'starts': 0,
|
||||
}
|
||||
|
||||
self.stopped = False
|
||||
|
|
@ -211,7 +205,7 @@ class SysMonitor(TracerCore):
|
|||
def __repr__(self) -> str:
|
||||
points = sum(len(v) for v in self.data.values())
|
||||
files = len(self.data)
|
||||
return f"<SysMonitor at {id(self):#x}: {points} data points in {files} files>"
|
||||
return f'<SysMonitor at {id(self):#x}: {points} data points in {files} files>'
|
||||
|
||||
@panopticon()
|
||||
def start(self) -> None:
|
||||
|
|
@ -219,7 +213,7 @@ class SysMonitor(TracerCore):
|
|||
self.stopped = False
|
||||
|
||||
assert sys_monitoring is not None
|
||||
sys_monitoring.use_tool_id(self.myid, "coverage.py")
|
||||
sys_monitoring.use_tool_id(self.myid, 'coverage.py')
|
||||
register = functools.partial(sys_monitoring.register_callback, self.myid)
|
||||
events = sys_monitoring.events
|
||||
if self.trace_arcs:
|
||||
|
|
@ -286,12 +280,12 @@ class SysMonitor(TracerCore):
|
|||
"""Get the frame of the Python code we're monitoring."""
|
||||
return inspect.currentframe().f_back.f_back # type: ignore[union-attr,return-value]
|
||||
|
||||
@panopticon("code", "@")
|
||||
@panopticon('code', '@')
|
||||
def sysmon_py_start(self, code: CodeType, instruction_offset: int) -> MonitorReturn:
|
||||
"""Handle sys.monitoring.events.PY_START events."""
|
||||
# Entering a new frame. Decide if we should trace in this file.
|
||||
self._activity = True
|
||||
self.stats["starts"] += 1
|
||||
self.stats['starts'] += 1
|
||||
|
||||
code_info = self.code_infos.get(id(code))
|
||||
tracing_code: bool | None = None
|
||||
|
|
@ -337,11 +331,11 @@ class SysMonitor(TracerCore):
|
|||
sys_monitoring.set_local_events(
|
||||
self.myid,
|
||||
code,
|
||||
events.PY_RETURN
|
||||
events.PY_RETURN |
|
||||
#
|
||||
| events.PY_RESUME
|
||||
events.PY_RESUME |
|
||||
# | events.PY_YIELD
|
||||
| events.LINE,
|
||||
events.LINE,
|
||||
# | events.BRANCH
|
||||
# | events.JUMP
|
||||
)
|
||||
|
|
@ -354,7 +348,7 @@ class SysMonitor(TracerCore):
|
|||
else:
|
||||
return sys.monitoring.DISABLE
|
||||
|
||||
@panopticon("code", "@")
|
||||
@panopticon('code', '@')
|
||||
def sysmon_py_resume_arcs(
|
||||
self, code: CodeType, instruction_offset: int,
|
||||
) -> MonitorReturn:
|
||||
|
|
@ -362,7 +356,7 @@ class SysMonitor(TracerCore):
|
|||
frame = self.callers_frame()
|
||||
self.last_lines[frame] = frame.f_lineno
|
||||
|
||||
@panopticon("code", "@", None)
|
||||
@panopticon('code', '@', None)
|
||||
def sysmon_py_return_arcs(
|
||||
self, code: CodeType, instruction_offset: int, retval: object,
|
||||
) -> MonitorReturn:
|
||||
|
|
@ -379,7 +373,7 @@ class SysMonitor(TracerCore):
|
|||
# Leaving this function, no need for the frame any more.
|
||||
self.last_lines.pop(frame, None)
|
||||
|
||||
@panopticon("code", "@", "exc")
|
||||
@panopticon('code', '@', 'exc')
|
||||
def sysmon_py_unwind_arcs(
|
||||
self, code: CodeType, instruction_offset: int, exception: BaseException,
|
||||
) -> MonitorReturn:
|
||||
|
|
@ -397,8 +391,7 @@ class SysMonitor(TracerCore):
|
|||
# log(f"adding {arc=}")
|
||||
cast(Set[TArc], code_info.file_data).add(arc)
|
||||
|
||||
|
||||
@panopticon("code", "line")
|
||||
@panopticon('code', 'line')
|
||||
def sysmon_line_lines(self, code: CodeType, line_number: int) -> MonitorReturn:
|
||||
"""Handle sys.monitoring.events.LINE events for line coverage."""
|
||||
code_info = self.code_infos[id(code)]
|
||||
|
|
@ -407,7 +400,7 @@ class SysMonitor(TracerCore):
|
|||
# log(f"adding {line_number=}")
|
||||
return sys.monitoring.DISABLE
|
||||
|
||||
@panopticon("code", "line")
|
||||
@panopticon('code', 'line')
|
||||
def sysmon_line_arcs(self, code: CodeType, line_number: int) -> MonitorReturn:
|
||||
"""Handle sys.monitoring.events.LINE events for branch coverage."""
|
||||
code_info = self.code_infos[id(code)]
|
||||
|
|
|
|||
|
|
@ -1,22 +1,20 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""A simple Python template renderer, for a nano-subset of Django syntax.
|
||||
|
||||
For a detailed discussion of this code, see this chapter from 500 Lines:
|
||||
http://aosabook.org/en/500L/a-template-engine.html
|
||||
|
||||
"""
|
||||
|
||||
# Coincidentally named the same as http://code.activestate.com/recipes/496702/
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from typing import (
|
||||
Any, Callable, Dict, NoReturn, cast,
|
||||
)
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import NoReturn
|
||||
|
||||
|
||||
class TempliteSyntaxError(ValueError):
|
||||
|
|
@ -37,7 +35,7 @@ class CodeBuilder:
|
|||
self.indent_level = indent
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "".join(str(c) for c in self.code)
|
||||
return ''.join(str(c) for c in self.code)
|
||||
|
||||
def add_line(self, line: str) -> None:
|
||||
"""Add a line of source to the code.
|
||||
|
|
@ -45,7 +43,7 @@ class CodeBuilder:
|
|||
Indentation and newline will be added for you, don't provide them.
|
||||
|
||||
"""
|
||||
self.code.extend([" " * self.indent_level, line, "\n"])
|
||||
self.code.extend([' ' * self.indent_level, line, '\n'])
|
||||
|
||||
def add_section(self) -> CodeBuilder:
|
||||
"""Add a section, a sub-CodeBuilder."""
|
||||
|
|
@ -117,6 +115,7 @@ class Templite:
|
|||
})
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, text: str, *contexts: dict[str, Any]) -> None:
|
||||
"""Construct a Templite with the given `text`.
|
||||
|
||||
|
|
@ -135,82 +134,82 @@ class Templite:
|
|||
# it, and execute it to render the template.
|
||||
code = CodeBuilder()
|
||||
|
||||
code.add_line("def render_function(context, do_dots):")
|
||||
code.add_line('def render_function(context, do_dots):')
|
||||
code.indent()
|
||||
vars_code = code.add_section()
|
||||
code.add_line("result = []")
|
||||
code.add_line("append_result = result.append")
|
||||
code.add_line("extend_result = result.extend")
|
||||
code.add_line("to_str = str")
|
||||
code.add_line('result = []')
|
||||
code.add_line('append_result = result.append')
|
||||
code.add_line('extend_result = result.extend')
|
||||
code.add_line('to_str = str')
|
||||
|
||||
buffered: list[str] = []
|
||||
|
||||
def flush_output() -> None:
|
||||
"""Force `buffered` to the code builder."""
|
||||
if len(buffered) == 1:
|
||||
code.add_line("append_result(%s)" % buffered[0])
|
||||
code.add_line('append_result(%s)' % buffered[0])
|
||||
elif len(buffered) > 1:
|
||||
code.add_line("extend_result([%s])" % ", ".join(buffered))
|
||||
code.add_line('extend_result([%s])' % ', '.join(buffered))
|
||||
del buffered[:]
|
||||
|
||||
ops_stack = []
|
||||
|
||||
# Split the text to form a list of tokens.
|
||||
tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
|
||||
tokens = re.split(r'(?s)({{.*?}}|{%.*?%}|{#.*?#})', text)
|
||||
|
||||
squash = in_joined = False
|
||||
|
||||
for token in tokens:
|
||||
if token.startswith("{"):
|
||||
if token.startswith('{'):
|
||||
start, end = 2, -2
|
||||
squash = (token[-3] == "-")
|
||||
squash = (token[-3] == '-')
|
||||
if squash:
|
||||
end = -3
|
||||
|
||||
if token.startswith("{#"):
|
||||
if token.startswith('{#'):
|
||||
# Comment: ignore it and move on.
|
||||
continue
|
||||
elif token.startswith("{{"):
|
||||
elif token.startswith('{{'):
|
||||
# An expression to evaluate.
|
||||
expr = self._expr_code(token[start:end].strip())
|
||||
buffered.append("to_str(%s)" % expr)
|
||||
buffered.append('to_str(%s)' % expr)
|
||||
else:
|
||||
# token.startswith("{%")
|
||||
# Action tag: split into words and parse further.
|
||||
flush_output()
|
||||
|
||||
words = token[start:end].strip().split()
|
||||
if words[0] == "if":
|
||||
if words[0] == 'if':
|
||||
# An if statement: evaluate the expression to determine if.
|
||||
if len(words) != 2:
|
||||
self._syntax_error("Don't understand if", token)
|
||||
ops_stack.append("if")
|
||||
code.add_line("if %s:" % self._expr_code(words[1]))
|
||||
ops_stack.append('if')
|
||||
code.add_line('if %s:' % self._expr_code(words[1]))
|
||||
code.indent()
|
||||
elif words[0] == "for":
|
||||
elif words[0] == 'for':
|
||||
# A loop: iterate over expression result.
|
||||
if len(words) != 4 or words[2] != "in":
|
||||
if len(words) != 4 or words[2] != 'in':
|
||||
self._syntax_error("Don't understand for", token)
|
||||
ops_stack.append("for")
|
||||
ops_stack.append('for')
|
||||
self._variable(words[1], self.loop_vars)
|
||||
code.add_line(
|
||||
f"for c_{words[1]} in {self._expr_code(words[3])}:",
|
||||
f'for c_{words[1]} in {self._expr_code(words[3])}:',
|
||||
)
|
||||
code.indent()
|
||||
elif words[0] == "joined":
|
||||
ops_stack.append("joined")
|
||||
elif words[0] == 'joined':
|
||||
ops_stack.append('joined')
|
||||
in_joined = True
|
||||
elif words[0].startswith("end"):
|
||||
elif words[0].startswith('end'):
|
||||
# Endsomething. Pop the ops stack.
|
||||
if len(words) != 1:
|
||||
self._syntax_error("Don't understand end", token)
|
||||
end_what = words[0][3:]
|
||||
if not ops_stack:
|
||||
self._syntax_error("Too many ends", token)
|
||||
self._syntax_error('Too many ends', token)
|
||||
start_what = ops_stack.pop()
|
||||
if start_what != end_what:
|
||||
self._syntax_error("Mismatched end tag", end_what)
|
||||
if end_what == "joined":
|
||||
self._syntax_error('Mismatched end tag', end_what)
|
||||
if end_what == 'joined':
|
||||
in_joined = False
|
||||
else:
|
||||
code.dedent()
|
||||
|
|
@ -219,19 +218,19 @@ class Templite:
|
|||
else:
|
||||
# Literal content. If it isn't empty, output it.
|
||||
if in_joined:
|
||||
token = re.sub(r"\s*\n\s*", "", token.strip())
|
||||
token = re.sub(r'\s*\n\s*', '', token.strip())
|
||||
elif squash:
|
||||
token = token.lstrip()
|
||||
if token:
|
||||
buffered.append(repr(token))
|
||||
|
||||
if ops_stack:
|
||||
self._syntax_error("Unmatched action tag", ops_stack[-1])
|
||||
self._syntax_error('Unmatched action tag', ops_stack[-1])
|
||||
|
||||
flush_output()
|
||||
|
||||
for var_name in self.all_vars - self.loop_vars:
|
||||
vars_code.add_line(f"c_{var_name} = context[{var_name!r}]")
|
||||
vars_code.add_line(f'c_{var_name} = context[{var_name!r}]')
|
||||
|
||||
code.add_line("return ''.join(result)")
|
||||
code.dedent()
|
||||
|
|
@ -240,30 +239,30 @@ class Templite:
|
|||
[Dict[str, Any], Callable[..., Any]],
|
||||
str,
|
||||
],
|
||||
code.get_globals()["render_function"],
|
||||
code.get_globals()['render_function'],
|
||||
)
|
||||
|
||||
def _expr_code(self, expr: str) -> str:
|
||||
"""Generate a Python expression for `expr`."""
|
||||
if "|" in expr:
|
||||
pipes = expr.split("|")
|
||||
if '|' in expr:
|
||||
pipes = expr.split('|')
|
||||
code = self._expr_code(pipes[0])
|
||||
for func in pipes[1:]:
|
||||
self._variable(func, self.all_vars)
|
||||
code = f"c_{func}({code})"
|
||||
elif "." in expr:
|
||||
dots = expr.split(".")
|
||||
code = f'c_{func}({code})'
|
||||
elif '.' in expr:
|
||||
dots = expr.split('.')
|
||||
code = self._expr_code(dots[0])
|
||||
args = ", ".join(repr(d) for d in dots[1:])
|
||||
code = f"do_dots({code}, {args})"
|
||||
args = ', '.join(repr(d) for d in dots[1:])
|
||||
code = f'do_dots({code}, {args})'
|
||||
else:
|
||||
self._variable(expr, self.all_vars)
|
||||
code = "c_%s" % expr
|
||||
code = 'c_%s' % expr
|
||||
return code
|
||||
|
||||
def _syntax_error(self, msg: str, thing: Any) -> NoReturn:
|
||||
"""Raise a syntax error using `msg`, and showing `thing`."""
|
||||
raise TempliteSyntaxError(f"{msg}: {thing!r}")
|
||||
raise TempliteSyntaxError(f'{msg}: {thing!r}')
|
||||
|
||||
def _variable(self, name: str, vars_set: set[str]) -> None:
|
||||
"""Track that `name` is used as a variable.
|
||||
|
|
@ -273,8 +272,8 @@ class Templite:
|
|||
Raises an syntax error if `name` is not a valid name.
|
||||
|
||||
"""
|
||||
if not re.match(r"[_a-zA-Z][_a-zA-Z0-9]*$", name):
|
||||
self._syntax_error("Not a valid name", name)
|
||||
if not re.match(r'[_a-zA-Z][_a-zA-Z0-9]*$', name):
|
||||
self._syntax_error('Not a valid name', name)
|
||||
vars_set.add(name)
|
||||
|
||||
def render(self, context: dict[str, Any] | None = None) -> str:
|
||||
|
|
|
|||
|
|
@ -1,27 +1,29 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""TOML configuration support for coverage.py"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from typing import Any, Callable, Iterable, TypeVar
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Iterable
|
||||
from typing import TypeVar
|
||||
|
||||
from coverage import env
|
||||
from coverage.exceptions import ConfigError
|
||||
from coverage.misc import import_third_party, substitute_variables
|
||||
from coverage.types import TConfigSectionOut, TConfigValueOut
|
||||
from coverage.misc import import_third_party
|
||||
from coverage.misc import substitute_variables
|
||||
from coverage.types import TConfigSectionOut
|
||||
from coverage.types import TConfigValueOut
|
||||
|
||||
|
||||
if env.PYVERSION >= (3, 11, 0, "alpha", 7):
|
||||
if env.PYVERSION >= (3, 11, 0, 'alpha', 7):
|
||||
import tomllib # pylint: disable=import-error
|
||||
has_tomllib = True
|
||||
else:
|
||||
# TOML support on Python 3.10 and below is an install-time extra option.
|
||||
tomllib, has_tomllib = import_third_party("tomli")
|
||||
tomllib, has_tomllib = import_third_party('tomli')
|
||||
|
||||
|
||||
class TomlDecodeError(Exception):
|
||||
|
|
@ -29,7 +31,8 @@ class TomlDecodeError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
TWant = TypeVar("TWant")
|
||||
TWant = TypeVar('TWant')
|
||||
|
||||
|
||||
class TomlConfigParser:
|
||||
"""TOML file reading with the interface of HandyConfigParser."""
|
||||
|
|
@ -60,7 +63,7 @@ class TomlConfigParser:
|
|||
raise TomlDecodeError(str(err)) from err
|
||||
return [filename]
|
||||
else:
|
||||
has_toml = re.search(r"^\[tool\.coverage(\.|])", toml_text, flags=re.MULTILINE)
|
||||
has_toml = re.search(r'^\[tool\.coverage(\.|])', toml_text, flags=re.MULTILINE)
|
||||
if self.our_file or has_toml:
|
||||
# Looks like they meant to read TOML, but we can't read it.
|
||||
msg = "Can't read {!r} without TOML support. Install with [toml] extra"
|
||||
|
|
@ -79,10 +82,10 @@ class TomlConfigParser:
|
|||
data (str): the dict of data in the section, or None if not found.
|
||||
|
||||
"""
|
||||
prefixes = ["tool.coverage."]
|
||||
prefixes = ['tool.coverage.']
|
||||
for prefix in prefixes:
|
||||
real_section = prefix + section
|
||||
parts = real_section.split(".")
|
||||
parts = real_section.split('.')
|
||||
try:
|
||||
data = self.data[parts[0]]
|
||||
for part in parts[1:]:
|
||||
|
|
@ -98,12 +101,12 @@ class TomlConfigParser:
|
|||
"""Like .get, but returns the real section name and the value."""
|
||||
name, data = self._get_section(section)
|
||||
if data is None:
|
||||
raise ConfigError(f"No section: {section!r}")
|
||||
raise ConfigError(f'No section: {section!r}')
|
||||
assert name is not None
|
||||
try:
|
||||
value = data[option]
|
||||
except KeyError:
|
||||
raise ConfigError(f"No option {option!r} in section: {name!r}") from None
|
||||
raise ConfigError(f'No option {option!r} in section: {name!r}') from None
|
||||
return name, value
|
||||
|
||||
def _get_single(self, section: str, option: str) -> Any:
|
||||
|
|
@ -134,7 +137,7 @@ class TomlConfigParser:
|
|||
def options(self, section: str) -> list[str]:
|
||||
_, data = self._get_section(section)
|
||||
if data is None:
|
||||
raise ConfigError(f"No section: {section!r}")
|
||||
raise ConfigError(f'No section: {section!r}')
|
||||
return list(data.keys())
|
||||
|
||||
def get_section(self, section: str) -> TConfigSectionOut:
|
||||
|
|
@ -168,18 +171,18 @@ class TomlConfigParser:
|
|||
f"Option [{section}]{option} couldn't convert to {type_desc}: {value!r}",
|
||||
) from e
|
||||
raise ValueError(
|
||||
f"Option [{section}]{option} is not {type_desc}: {value!r}",
|
||||
f'Option [{section}]{option} is not {type_desc}: {value!r}',
|
||||
)
|
||||
|
||||
def getboolean(self, section: str, option: str) -> bool:
|
||||
name, value = self._get_single(section, option)
|
||||
bool_strings = {"true": True, "false": False}
|
||||
return self._check_type(name, option, value, bool, bool_strings.__getitem__, "a boolean")
|
||||
bool_strings = {'true': True, 'false': False}
|
||||
return self._check_type(name, option, value, bool, bool_strings.__getitem__, 'a boolean')
|
||||
|
||||
def _get_list(self, section: str, option: str) -> tuple[str, list[str]]:
|
||||
"""Get a list of strings, substituting environment variables in the elements."""
|
||||
name, values = self._get(section, option)
|
||||
values = self._check_type(name, option, values, list, None, "a list")
|
||||
values = self._check_type(name, option, values, list, None, 'a list')
|
||||
values = [substitute_variables(value, os.environ) for value in values]
|
||||
return name, values
|
||||
|
||||
|
|
@ -194,15 +197,15 @@ class TomlConfigParser:
|
|||
try:
|
||||
re.compile(value)
|
||||
except re.error as e:
|
||||
raise ConfigError(f"Invalid [{name}].{option} value {value!r}: {e}") from e
|
||||
raise ConfigError(f'Invalid [{name}].{option} value {value!r}: {e}') from e
|
||||
return values
|
||||
|
||||
def getint(self, section: str, option: str) -> int:
|
||||
name, value = self._get_single(section, option)
|
||||
return self._check_type(name, option, value, int, int, "an integer")
|
||||
return self._check_type(name, option, value, int, int, 'an integer')
|
||||
|
||||
def getfloat(self, section: str, option: str) -> float:
|
||||
name, value = self._get_single(section, option)
|
||||
if isinstance(value, int):
|
||||
value = float(value)
|
||||
return self._check_type(name, option, value, float, float, "a float")
|
||||
return self._check_type(name, option, value, float, float, 'a float')
|
||||
|
|
|
|||
|
|
@ -1,20 +1,27 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""
|
||||
Types for use throughout coverage.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from types import FrameType, ModuleType
|
||||
from typing import (
|
||||
Any, Callable, Dict, Iterable, List, Mapping, Optional, Protocol,
|
||||
Set, Tuple, Type, Union, TYPE_CHECKING,
|
||||
)
|
||||
from types import FrameType
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Protocol
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from coverage.plugin import FileTracer
|
||||
|
|
@ -22,7 +29,7 @@ if TYPE_CHECKING:
|
|||
|
||||
AnyCallable = Callable[..., Any]
|
||||
|
||||
## File paths
|
||||
# File paths
|
||||
|
||||
# For arguments that are file paths:
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -34,10 +41,12 @@ else:
|
|||
FilePathClasses = [str, pathlib.Path]
|
||||
FilePathType = Union[Type[str], Type[pathlib.Path]]
|
||||
|
||||
## Python tracing
|
||||
# Python tracing
|
||||
|
||||
|
||||
class TTraceFn(Protocol):
|
||||
"""A Python trace function."""
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
frame: FrameType,
|
||||
|
|
@ -47,13 +56,15 @@ class TTraceFn(Protocol):
|
|||
) -> TTraceFn | None:
|
||||
...
|
||||
|
||||
## Coverage.py tracing
|
||||
# Coverage.py tracing
|
||||
|
||||
|
||||
# Line numbers are pervasive enough that they deserve their own type.
|
||||
TLineNo = int
|
||||
|
||||
TArc = Tuple[TLineNo, TLineNo]
|
||||
|
||||
|
||||
class TFileDisposition(Protocol):
|
||||
"""A simple value type for recording what to do with a file."""
|
||||
|
||||
|
|
@ -78,6 +89,7 @@ TTraceFileData = Union[Set[TLineNo], Set[TArc], Set[int]]
|
|||
|
||||
TTraceData = Dict[str, TTraceFileData]
|
||||
|
||||
|
||||
class TracerCore(Protocol):
|
||||
"""Anything that can report on Python execution."""
|
||||
|
||||
|
|
@ -108,13 +120,13 @@ class TracerCore(Protocol):
|
|||
"""Return a dictionary of statistics, or None."""
|
||||
|
||||
|
||||
## Coverage
|
||||
# Coverage
|
||||
|
||||
# Many places use kwargs as Coverage kwargs.
|
||||
TCovKwargs = Any
|
||||
|
||||
|
||||
## Configuration
|
||||
# Configuration
|
||||
|
||||
# One value read from a config file.
|
||||
TConfigValueIn = Optional[Union[bool, int, float, str, Iterable[str]]]
|
||||
|
|
@ -123,6 +135,7 @@ TConfigValueOut = Optional[Union[bool, int, float, str, List[str]]]
|
|||
TConfigSectionIn = Mapping[str, TConfigValueIn]
|
||||
TConfigSectionOut = Mapping[str, TConfigValueOut]
|
||||
|
||||
|
||||
class TConfigurable(Protocol):
|
||||
"""Something that can proxy to the coverage configuration settings."""
|
||||
|
||||
|
|
@ -148,6 +161,7 @@ class TConfigurable(Protocol):
|
|||
|
||||
"""
|
||||
|
||||
|
||||
class TPluginConfig(Protocol):
|
||||
"""Something that can provide options to a plugin."""
|
||||
|
||||
|
|
@ -155,13 +169,14 @@ class TPluginConfig(Protocol):
|
|||
"""Get the options for a plugin."""
|
||||
|
||||
|
||||
## Parsing
|
||||
# Parsing
|
||||
|
||||
TMorf = Union[ModuleType, str]
|
||||
|
||||
TSourceTokenLines = Iterable[List[Tuple[str, str]]]
|
||||
|
||||
## Plugins
|
||||
# Plugins
|
||||
|
||||
|
||||
class TPlugin(Protocol):
|
||||
"""What all plugins have in common."""
|
||||
|
|
@ -169,10 +184,11 @@ class TPlugin(Protocol):
|
|||
_coverage_enabled: bool
|
||||
|
||||
|
||||
## Debugging
|
||||
# Debugging
|
||||
|
||||
class TWarnFn(Protocol):
|
||||
"""A callable warn() function."""
|
||||
|
||||
def __call__(self, msg: str, slug: str | None = None, once: bool = False) -> None:
|
||||
...
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""The version and URL for coverage.py"""
|
||||
# This file is exec'ed in setup.py, don't import anything!
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# version_info: same semantics as sys.version_info.
|
||||
# _dev: the .devN suffix if any.
|
||||
version_info = (7, 4, 4, "final", 0)
|
||||
version_info = (7, 4, 4, 'final', 0)
|
||||
_dev = 0
|
||||
|
||||
|
||||
|
|
@ -16,18 +14,18 @@ def _make_version(
|
|||
major: int,
|
||||
minor: int,
|
||||
micro: int,
|
||||
releaselevel: str = "final",
|
||||
releaselevel: str = 'final',
|
||||
serial: int = 0,
|
||||
dev: int = 0,
|
||||
) -> str:
|
||||
"""Create a readable version string from version_info tuple components."""
|
||||
assert releaselevel in ["alpha", "beta", "candidate", "final"]
|
||||
version = "%d.%d.%d" % (major, minor, micro)
|
||||
if releaselevel != "final":
|
||||
short = {"alpha": "a", "beta": "b", "candidate": "rc"}[releaselevel]
|
||||
version += f"{short}{serial}"
|
||||
assert releaselevel in ['alpha', 'beta', 'candidate', 'final']
|
||||
version = '%d.%d.%d' % (major, minor, micro)
|
||||
if releaselevel != 'final':
|
||||
short = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc'}[releaselevel]
|
||||
version += f'{short}{serial}'
|
||||
if dev != 0:
|
||||
version += f".dev{dev}"
|
||||
version += f'.dev{dev}'
|
||||
return version
|
||||
|
||||
|
||||
|
|
@ -41,8 +39,8 @@ def _make_url(
|
|||
) -> str:
|
||||
"""Make the URL people should start at for this version of coverage.py."""
|
||||
return (
|
||||
"https://coverage.readthedocs.io/en/"
|
||||
+ _make_version(major, minor, micro, releaselevel, serial, dev)
|
||||
'https://coverage.readthedocs.io/en/' +
|
||||
_make_version(major, minor, micro, releaselevel, serial, dev)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""XML reporting for coverage.py"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import time
|
||||
import xml.dom.minidom
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, IO, Iterable, TYPE_CHECKING
|
||||
from typing import Any
|
||||
from typing import IO
|
||||
from typing import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from coverage import __version__, files
|
||||
from coverage.misc import isolate_module, human_sorted, human_sorted_items
|
||||
from coverage import __version__
|
||||
from coverage import files
|
||||
from coverage.misc import human_sorted
|
||||
from coverage.misc import human_sorted_items
|
||||
from coverage.misc import isolate_module
|
||||
from coverage.plugin import FileReporter
|
||||
from coverage.report_core import get_analysis_to_report
|
||||
from coverage.results import Analysis
|
||||
|
|
@ -28,15 +30,15 @@ if TYPE_CHECKING:
|
|||
os = isolate_module(os)
|
||||
|
||||
|
||||
DTD_URL = "https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd"
|
||||
DTD_URL = 'https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd'
|
||||
|
||||
|
||||
def rate(hit: int, num: int) -> str:
|
||||
"""Return the fraction of `hit`/`num`, as a string."""
|
||||
if num == 0:
|
||||
return "1"
|
||||
return '1'
|
||||
else:
|
||||
return "%.4g" % (hit / num)
|
||||
return '%.4g' % (hit / num)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -57,7 +59,7 @@ def appendChild(parent: Any, child: Any) -> None:
|
|||
class XmlReporter:
|
||||
"""A reporter for writing Cobertura-style XML coverage results."""
|
||||
|
||||
report_type = "XML report"
|
||||
report_type = 'XML report'
|
||||
|
||||
def __init__(self, coverage: Coverage) -> None:
|
||||
self.coverage = coverage
|
||||
|
|
@ -68,7 +70,7 @@ class XmlReporter:
|
|||
for src in self.config.source:
|
||||
if os.path.exists(src):
|
||||
if self.config.relative_files:
|
||||
src = src.rstrip(r"\/")
|
||||
src = src.rstrip(r'\/')
|
||||
else:
|
||||
src = files.canonical_filename(src)
|
||||
self.source_paths.add(src)
|
||||
|
|
@ -90,27 +92,29 @@ class XmlReporter:
|
|||
# Create the DOM that will store the data.
|
||||
impl = xml.dom.minidom.getDOMImplementation()
|
||||
assert impl is not None
|
||||
self.xml_out = impl.createDocument(None, "coverage", None)
|
||||
self.xml_out = impl.createDocument(None, 'coverage', None)
|
||||
|
||||
# Write header stuff.
|
||||
xcoverage = self.xml_out.documentElement
|
||||
xcoverage.setAttribute("version", __version__)
|
||||
xcoverage.setAttribute("timestamp", str(int(time.time()*1000)))
|
||||
xcoverage.appendChild(self.xml_out.createComment(
|
||||
f" Generated by coverage.py: {__url__} ",
|
||||
))
|
||||
xcoverage.appendChild(self.xml_out.createComment(f" Based on {DTD_URL} "))
|
||||
xcoverage.setAttribute('version', __version__)
|
||||
xcoverage.setAttribute('timestamp', str(int(time.time() * 1000)))
|
||||
xcoverage.appendChild(
|
||||
self.xml_out.createComment(
|
||||
f' Generated by coverage.py: {__url__} ',
|
||||
),
|
||||
)
|
||||
xcoverage.appendChild(self.xml_out.createComment(f' Based on {DTD_URL} '))
|
||||
|
||||
# Call xml_file for each file in the data.
|
||||
for fr, analysis in get_analysis_to_report(self.coverage, morfs):
|
||||
self.xml_file(fr, analysis, has_arcs)
|
||||
|
||||
xsources = self.xml_out.createElement("sources")
|
||||
xsources = self.xml_out.createElement('sources')
|
||||
xcoverage.appendChild(xsources)
|
||||
|
||||
# Populate the XML DOM with the source info.
|
||||
for path in human_sorted(self.source_paths):
|
||||
xsource = self.xml_out.createElement("source")
|
||||
xsource = self.xml_out.createElement('source')
|
||||
appendChild(xsources, xsource)
|
||||
txt = self.xml_out.createTextNode(path)
|
||||
appendChild(xsource, txt)
|
||||
|
|
@ -118,43 +122,43 @@ class XmlReporter:
|
|||
lnum_tot, lhits_tot = 0, 0
|
||||
bnum_tot, bhits_tot = 0, 0
|
||||
|
||||
xpackages = self.xml_out.createElement("packages")
|
||||
xpackages = self.xml_out.createElement('packages')
|
||||
xcoverage.appendChild(xpackages)
|
||||
|
||||
# Populate the XML DOM with the package info.
|
||||
for pkg_name, pkg_data in human_sorted_items(self.packages.items()):
|
||||
xpackage = self.xml_out.createElement("package")
|
||||
xpackage = self.xml_out.createElement('package')
|
||||
appendChild(xpackages, xpackage)
|
||||
xclasses = self.xml_out.createElement("classes")
|
||||
xclasses = self.xml_out.createElement('classes')
|
||||
appendChild(xpackage, xclasses)
|
||||
for _, class_elt in human_sorted_items(pkg_data.elements.items()):
|
||||
appendChild(xclasses, class_elt)
|
||||
xpackage.setAttribute("name", pkg_name.replace(os.sep, "."))
|
||||
xpackage.setAttribute("line-rate", rate(pkg_data.hits, pkg_data.lines))
|
||||
xpackage.setAttribute('name', pkg_name.replace(os.sep, '.'))
|
||||
xpackage.setAttribute('line-rate', rate(pkg_data.hits, pkg_data.lines))
|
||||
if has_arcs:
|
||||
branch_rate = rate(pkg_data.br_hits, pkg_data.branches)
|
||||
else:
|
||||
branch_rate = "0"
|
||||
xpackage.setAttribute("branch-rate", branch_rate)
|
||||
xpackage.setAttribute("complexity", "0")
|
||||
branch_rate = '0'
|
||||
xpackage.setAttribute('branch-rate', branch_rate)
|
||||
xpackage.setAttribute('complexity', '0')
|
||||
|
||||
lhits_tot += pkg_data.hits
|
||||
lnum_tot += pkg_data.lines
|
||||
bhits_tot += pkg_data.br_hits
|
||||
bnum_tot += pkg_data.branches
|
||||
|
||||
xcoverage.setAttribute("lines-valid", str(lnum_tot))
|
||||
xcoverage.setAttribute("lines-covered", str(lhits_tot))
|
||||
xcoverage.setAttribute("line-rate", rate(lhits_tot, lnum_tot))
|
||||
xcoverage.setAttribute('lines-valid', str(lnum_tot))
|
||||
xcoverage.setAttribute('lines-covered', str(lhits_tot))
|
||||
xcoverage.setAttribute('line-rate', rate(lhits_tot, lnum_tot))
|
||||
if has_arcs:
|
||||
xcoverage.setAttribute("branches-valid", str(bnum_tot))
|
||||
xcoverage.setAttribute("branches-covered", str(bhits_tot))
|
||||
xcoverage.setAttribute("branch-rate", rate(bhits_tot, bnum_tot))
|
||||
xcoverage.setAttribute('branches-valid', str(bnum_tot))
|
||||
xcoverage.setAttribute('branches-covered', str(bhits_tot))
|
||||
xcoverage.setAttribute('branch-rate', rate(bhits_tot, bnum_tot))
|
||||
else:
|
||||
xcoverage.setAttribute("branches-covered", "0")
|
||||
xcoverage.setAttribute("branches-valid", "0")
|
||||
xcoverage.setAttribute("branch-rate", "0")
|
||||
xcoverage.setAttribute("complexity", "0")
|
||||
xcoverage.setAttribute('branches-covered', '0')
|
||||
xcoverage.setAttribute('branches-valid', '0')
|
||||
xcoverage.setAttribute('branch-rate', '0')
|
||||
xcoverage.setAttribute('complexity', '0')
|
||||
|
||||
# Write the output file.
|
||||
outfile.write(serialize_xml(self.xml_out))
|
||||
|
|
@ -176,57 +180,57 @@ class XmlReporter:
|
|||
|
||||
# Create the "lines" and "package" XML elements, which
|
||||
# are populated later. Note that a package == a directory.
|
||||
filename = fr.filename.replace("\\", "/")
|
||||
filename = fr.filename.replace('\\', '/')
|
||||
for source_path in self.source_paths:
|
||||
if not self.config.relative_files:
|
||||
source_path = files.canonical_filename(source_path)
|
||||
if filename.startswith(source_path.replace("\\", "/") + "/"):
|
||||
rel_name = filename[len(source_path)+1:]
|
||||
if filename.startswith(source_path.replace('\\', '/') + '/'):
|
||||
rel_name = filename[len(source_path) + 1:]
|
||||
break
|
||||
else:
|
||||
rel_name = fr.relative_filename().replace("\\", "/")
|
||||
self.source_paths.add(fr.filename[:-len(rel_name)].rstrip(r"\/"))
|
||||
rel_name = fr.relative_filename().replace('\\', '/')
|
||||
self.source_paths.add(fr.filename[:-len(rel_name)].rstrip(r'\/'))
|
||||
|
||||
dirname = os.path.dirname(rel_name) or "."
|
||||
dirname = "/".join(dirname.split("/")[:self.config.xml_package_depth])
|
||||
package_name = dirname.replace("/", ".")
|
||||
dirname = os.path.dirname(rel_name) or '.'
|
||||
dirname = '/'.join(dirname.split('/')[:self.config.xml_package_depth])
|
||||
package_name = dirname.replace('/', '.')
|
||||
|
||||
package = self.packages.setdefault(package_name, PackageData({}, 0, 0, 0, 0))
|
||||
|
||||
xclass: xml.dom.minidom.Element = self.xml_out.createElement("class")
|
||||
xclass: xml.dom.minidom.Element = self.xml_out.createElement('class')
|
||||
|
||||
appendChild(xclass, self.xml_out.createElement("methods"))
|
||||
appendChild(xclass, self.xml_out.createElement('methods'))
|
||||
|
||||
xlines = self.xml_out.createElement("lines")
|
||||
xlines = self.xml_out.createElement('lines')
|
||||
appendChild(xclass, xlines)
|
||||
|
||||
xclass.setAttribute("name", os.path.relpath(rel_name, dirname))
|
||||
xclass.setAttribute("filename", rel_name.replace("\\", "/"))
|
||||
xclass.setAttribute("complexity", "0")
|
||||
xclass.setAttribute('name', os.path.relpath(rel_name, dirname))
|
||||
xclass.setAttribute('filename', rel_name.replace('\\', '/'))
|
||||
xclass.setAttribute('complexity', '0')
|
||||
|
||||
branch_stats = analysis.branch_stats()
|
||||
missing_branch_arcs = analysis.missing_branch_arcs()
|
||||
|
||||
# For each statement, create an XML "line" element.
|
||||
for line in sorted(analysis.statements):
|
||||
xline = self.xml_out.createElement("line")
|
||||
xline.setAttribute("number", str(line))
|
||||
xline = self.xml_out.createElement('line')
|
||||
xline.setAttribute('number', str(line))
|
||||
|
||||
# Q: can we get info about the number of times a statement is
|
||||
# executed? If so, that should be recorded here.
|
||||
xline.setAttribute("hits", str(int(line not in analysis.missing)))
|
||||
xline.setAttribute('hits', str(int(line not in analysis.missing)))
|
||||
|
||||
if has_arcs:
|
||||
if line in branch_stats:
|
||||
total, taken = branch_stats[line]
|
||||
xline.setAttribute("branch", "true")
|
||||
xline.setAttribute('branch', 'true')
|
||||
xline.setAttribute(
|
||||
"condition-coverage",
|
||||
"%d%% (%d/%d)" % (100*taken//total, taken, total),
|
||||
'condition-coverage',
|
||||
'%d%% (%d/%d)' % (100 * taken // total, taken, total),
|
||||
)
|
||||
if line in missing_branch_arcs:
|
||||
annlines = ["exit" if b < 0 else str(b) for b in missing_branch_arcs[line]]
|
||||
xline.setAttribute("missing-branches", ",".join(annlines))
|
||||
annlines = ['exit' if b < 0 else str(b) for b in missing_branch_arcs[line]]
|
||||
xline.setAttribute('missing-branches', ','.join(annlines))
|
||||
appendChild(xlines, xline)
|
||||
|
||||
class_lines = len(analysis.statements)
|
||||
|
|
@ -241,12 +245,12 @@ class XmlReporter:
|
|||
class_br_hits = 0
|
||||
|
||||
# Finalize the statistics that are collected in the XML DOM.
|
||||
xclass.setAttribute("line-rate", rate(class_hits, class_lines))
|
||||
xclass.setAttribute('line-rate', rate(class_hits, class_lines))
|
||||
if has_arcs:
|
||||
branch_rate = rate(class_br_hits, class_branches)
|
||||
else:
|
||||
branch_rate = "0"
|
||||
xclass.setAttribute("branch-rate", branch_rate)
|
||||
branch_rate = '0'
|
||||
xclass.setAttribute('branch-rate', branch_rate)
|
||||
|
||||
package.elements[rel_name] = xclass
|
||||
package.hits += class_hits
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue