[pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
This commit is contained in:
pre-commit-ci[bot] 2024-04-13 00:00:18 +00:00
parent 72ad6dc953
commit f4cd1ba0d6
813 changed files with 66015 additions and 58839 deletions

View file

@ -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

View file

@ -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())

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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__)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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.

View file

@ -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)

View file

@ -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 "&nbsp;"
tok_html = escape(tok_text) or '&nbsp;'
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 = ",&nbsp;&nbsp; ".join(
f"{ldata.number}&#x202F;&#x219B;&#x202F;{d}"
ldata.annotate = ',&nbsp;&nbsp; '.join(
f'{ldata.number}&#x202F;&#x219B;&#x202F;{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("&", "&amp;").replace("<", "&lt;")
return t.replace('&', '&amp;').replace('<', '&lt;')
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)

View file

@ -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

View file

@ -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

View 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')

View file

@ -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

View file

@ -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

View file

@ -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)))

View file

@ -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')

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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:

View file

@ -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

View file

@ -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:

View file

@ -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.

View file

@ -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

View file

@ -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())

View file

@ -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)]

View file

@ -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:

View file

@ -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')

View file

@ -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:
...

View file

@ -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)
)

View file

@ -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