Coverage report: + 94% +
+ + ++ coverage.py v7.2.3a0.dev1, + created at 2023-03-21 08:44 -0400 +
+diff -Nru python-coverage-6.5.0+dfsg1/CHANGES.rst python-coverage-7.2.7+dfsg1/CHANGES.rst
--- python-coverage-6.5.0+dfsg1/CHANGES.rst 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/CHANGES.rst 2023-05-29 19:46:30.000000000 +0000
@@ -17,13 +17,475 @@
.. Version 9.8.1 — 2027-07-27
.. --------------------------
+.. scriv-start-here
+
+.. _changes_7-2-7:
+
+Version 7.2.7 — 2023-05-29
+--------------------------
+
+- Fix: reverted a `change from 6.4.3
"
- self.text = text
- if not self.text:
+ if text is not None:
+ self.text: str = text
+ else:
from coverage.python import get_python_source
try:
self.text = get_python_source(self.filename)
@@ -46,45 +60,45 @@
self.exclude = exclude
# The text lines of the parsed code.
- self.lines = 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
# lines.
- self.statements = set()
+ self.statements: Set[TLineNo] = set()
# The normalized line numbers of the excluded lines in the code,
# adjusted to their first lines.
- self.excluded = set()
+ self.excluded: Set[TLineNo] = set()
# The raw_* attributes are only used in this class, and in
# lab/parser.py to show how this class is working.
# The line numbers that start statements, as reported by the line
# number table in the bytecode.
- self.raw_statements = set()
+ self.raw_statements: Set[TLineNo] = set()
# The raw line numbers of excluded lines of code, as marked by pragmas.
- self.raw_excluded = set()
+ self.raw_excluded: Set[TLineNo] = set()
# The line numbers of class definitions.
- self.raw_classdefs = set()
+ self.raw_classdefs: Set[TLineNo] = set()
# The line numbers of docstring lines.
- self.raw_docstrings = set()
+ self.raw_docstrings: Set[TLineNo] = set()
# Internal detail, used by lab/parser.py.
self.show_tokens = False
# A dict mapping line numbers to lexical statement starts for
# multi-line statements.
- self._multiline = {}
+ self._multiline: Dict[TLineNo, TLineNo] = {}
# Lazily-created arc data, and missing arc descriptions.
- self._all_arcs = None
- self._missing_arc_fragments = None
+ self._all_arcs: Optional[Set[TArc]] = None
+ self._missing_arc_fragments: Optional[TArcFragments] = None
- def lines_matching(self, *regexes):
+ def lines_matching(self, *regexes: str) -> Set[TLineNo]:
"""Find the lines matching one of a list of regexes.
Returns a set of line numbers, the lines that contain a match for one
@@ -100,7 +114,7 @@
matches.add(i)
return matches
- def _raw_parse(self):
+ def _raw_parse(self) -> None:
"""Parse the source to find the interesting facts about its lines.
A handful of attributes are updated.
@@ -122,6 +136,7 @@
first_on_line = True
nesting = 0
+ assert self.text is not None
tokgen = generate_tokens(self.text)
for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen:
if self.show_tokens: # pragma: debugging
@@ -134,13 +149,13 @@
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.
+ # 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 = (elineno in self.raw_excluded) or excluding_decorators
if not excluding and should_exclude:
# Start excluding a suite. We trigger off of the colon
@@ -150,7 +165,7 @@
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
@@ -167,21 +182,20 @@
# http://stackoverflow.com/questions/1769332/x/1769794#1769794
self.raw_docstrings.update(range(slineno, elineno+1))
elif toktype == token.NEWLINE:
- if first_line is not None and elineno != first_line:
+ if first_line is not None and elineno != first_line: # type: ignore[unreachable]
# 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): # type: ignore[unreachable]
self._multiline[l] = first_line
first_line = None
first_on_line = True
if ttext.strip() and toktype != tokenize.COMMENT:
- # A non-whitespace token.
+ # A non-white-space token.
empty = False
if first_line is None:
- # The token is not whitespace, and is the first in a
- # statement.
+ # The token is not white space, and is the first in a statement.
first_line = slineno
# Check whether to end an excluded suite.
if excluding and indent <= exclude_indent:
@@ -203,32 +217,32 @@
if env.PYBEHAVIOR.module_firstline_1 and self._multiline:
self._multiline[1] = min(self.raw_statements)
- def first_line(self, line):
- """Return the first line number of the statement including `line`."""
- if line < 0:
- line = -self._multiline.get(-line, -line)
+ def first_line(self, lineno: TLineNo) -> TLineNo:
+ """Return the first line number of the statement including `lineno`."""
+ if lineno < 0:
+ lineno = -self._multiline.get(-lineno, -lineno)
else:
- line = self._multiline.get(line, line)
- return line
+ lineno = self._multiline.get(lineno, lineno)
+ return lineno
- def first_lines(self, lines):
- """Map the line numbers in `lines` to the correct first line of the
+ def first_lines(self, linenos: Iterable[TLineNo]) -> Set[TLineNo]:
+ """Map the line numbers in `linenos` to the correct first line of the
statement.
Returns a set of the first lines.
"""
- return {self.first_line(l) for l in lines}
+ return {self.first_line(l) for l in linenos}
- def translate_lines(self, lines):
+ def translate_lines(self, lines: Iterable[TLineNo]) -> Set[TLineNo]:
"""Implement `FileReporter.translate_lines`."""
return self.first_lines(lines)
- def translate_arcs(self, arcs):
+ def translate_arcs(self, arcs: Iterable[TArc]) -> Set[TArc]:
"""Implement `FileReporter.translate_arcs`."""
- return [(self.first_line(a), self.first_line(b)) for (a, b) in arcs]
+ return {(self.first_line(a), self.first_line(b)) for (a, b) in arcs}
- def parse_source(self):
+ def parse_source(self) -> None:
"""Parse source text to find executable lines, excluded lines, etc.
Sets the .excluded and .statements attributes, normalized to the first
@@ -237,7 +251,7 @@
"""
try:
self._raw_parse()
- except (tokenize.TokenError, IndentationError) as err:
+ except (tokenize.TokenError, IndentationError, SyntaxError) as err:
if hasattr(err, "lineno"):
lineno = err.lineno # IndentationError
else:
@@ -253,7 +267,7 @@
starts = self.raw_statements - ignore
self.statements = self.first_lines(starts) - ignore
- def arcs(self):
+ def arcs(self) -> Set[TArc]:
"""Get information about the arcs available in the code.
Returns a set of line number pairs. Line numbers have been normalized
@@ -262,9 +276,10 @@
"""
if self._all_arcs is None:
self._analyze_ast()
+ assert self._all_arcs is not None
return self._all_arcs
- def _analyze_ast(self):
+ def _analyze_ast(self) -> None:
"""Run the AstArcAnalyzer and save its results.
`_all_arcs` is the set of arcs in the code.
@@ -282,13 +297,13 @@
self._missing_arc_fragments = aaa.missing_arc_fragments
- def exit_counts(self):
+ def exit_counts(self) -> Dict[TLineNo, int]:
"""Get a count of exits from that each line.
Excluded lines are excluded.
"""
- exit_counts = collections.defaultdict(int)
+ exit_counts: Dict[TLineNo, int] = collections.defaultdict(int)
for l1, l2 in self.arcs():
if l1 < 0:
# Don't ever report -1 as a line number
@@ -309,10 +324,16 @@
return exit_counts
- def missing_arc_description(self, start, end, executed_arcs=None):
+ def missing_arc_description(
+ self,
+ start: TLineNo,
+ end: TLineNo,
+ executed_arcs: Optional[Iterable[TArc]] = None,
+ ) -> str:
"""Provide an English sentence describing a missing arc."""
if self._missing_arc_fragments is None:
self._analyze_ast()
+ assert self._missing_arc_fragments is not None
actual_start = start
@@ -352,31 +373,27 @@
class ByteParser:
"""Parse bytecode to understand the structure of code."""
- @contract(text='unicode')
- def __init__(self, text, code=None, filename=None):
+ def __init__(
+ self,
+ text: str,
+ code: Optional[CodeType] = None,
+ filename: Optional[str] = None,
+ ) -> None:
self.text = text
- if code:
+ if code is not None:
self.code = code
else:
+ assert filename is not None
try:
- self.code = compile_unicode(text, filename, "exec")
+ 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" % (
- filename, synerr.msg, synerr.lineno
+ filename, synerr.msg, synerr.lineno or 0
)
) from synerr
- # Alternative Python implementations don't always provide all the
- # attributes on code objects that we need to do the analysis.
- for attr in ['co_lnotab', 'co_firstlineno']:
- if not hasattr(self.code, attr):
- raise _StopEverything( # pragma: only jython
- "This implementation of Python doesn't support code analysis.\n" +
- "Run coverage.py under another Python for this command."
- )
-
- def child_parsers(self):
+ def child_parsers(self) -> Iterable[ByteParser]:
"""Iterate over all the code objects nested within this one.
The iteration includes `self` as its first value.
@@ -384,7 +401,7 @@
"""
return (ByteParser(self.text, code=c) for c in code_objects(self.code))
- def _line_numbers(self):
+ def _line_numbers(self) -> Iterable[TLineNo]:
"""Yield the line numbers possible in this code object.
Uses co_lnotab described in Python/compile.c to find the
@@ -414,7 +431,7 @@
if line_num != last_line_num:
yield line_num
- def _find_statements(self):
+ def _find_statements(self) -> Iterable[TLineNo]:
"""Find the statements in `self.code`.
Produce a sequence of line numbers that start statements. Recurses
@@ -430,7 +447,36 @@
# AST analysis
#
-class BlockBase:
+class ArcStart(collections.namedtuple("Arc", "lineno, cause")):
+ """The information needed to start an arc.
+
+ `lineno` is the line number the arc starts from.
+
+ `cause` is an English text fragment used as the `startmsg` for
+ AstArcAnalyzer.missing_arc_fragments. It will be used to describe why an
+ arc wasn't executed, so should fit well into a sentence of the form,
+ "Line 17 didn't run because {cause}." The fragment can include "{lineno}"
+ to have `lineno` interpolated into it.
+
+ """
+ def __new__(cls, lineno: TLineNo, cause: Optional[str] = None) -> ArcStart:
+ return super().__new__(cls, lineno, cause)
+
+
+class TAddArcFn(Protocol):
+ """The type for AstArcAnalyzer.add_arc()."""
+ def __call__(
+ self,
+ start: TLineNo,
+ end: TLineNo,
+ smsg: Optional[str] = None,
+ emsg: Optional[str] = None,
+ ) -> None:
+ ...
+
+TArcFragments = Dict[TArc, List[Tuple[Optional[str], Optional[str]]]]
+
+class Block:
"""
Blocks need to handle various exiting statements in their own ways.
@@ -440,56 +486,54 @@
stack.
"""
# pylint: disable=unused-argument
- def process_break_exits(self, exits, add_arc):
+ 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
# implement process_break_exits, this function is never reached.
raise AssertionError
- def process_continue_exits(self, exits, add_arc):
+ def process_continue_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool:
"""Process continue exits."""
# Because continue can only appear in loops, and most subclasses
# implement process_continue_exits, this function is never reached.
raise AssertionError
- def process_raise_exits(self, exits, add_arc):
+ def process_raise_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool:
"""Process raise exits."""
return False
- def process_return_exits(self, exits, add_arc):
+ def process_return_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool:
"""Process return exits."""
return False
-class LoopBlock(BlockBase):
+class LoopBlock(Block):
"""A block on the block stack representing a `for` or `while` loop."""
- @contract(start=int)
- def __init__(self, start):
+ def __init__(self, start: TLineNo) -> None:
# The line number where the loop starts.
self.start = start
# A set of ArcStarts, the arcs from break statements exiting this loop.
- self.break_exits = set()
+ self.break_exits: Set[ArcStart] = set()
- def process_break_exits(self, exits, add_arc):
+ def process_break_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool:
self.break_exits.update(exits)
return True
- def process_continue_exits(self, exits, add_arc):
+ def process_continue_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool:
for xit in exits:
add_arc(xit.lineno, self.start, xit.cause)
return True
-class FunctionBlock(BlockBase):
+class FunctionBlock(Block):
"""A block on the block stack representing a function definition."""
- @contract(start=int, name=str)
- def __init__(self, start, name):
+ def __init__(self, start: TLineNo, name: str) -> None:
# The line number where the function starts.
self.start = start
# The name of the function.
self.name = name
- def process_raise_exits(self, exits, add_arc):
+ def process_raise_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool:
for xit in exits:
add_arc(
xit.lineno, -self.start, xit.cause,
@@ -497,7 +541,7 @@
)
return True
- def process_return_exits(self, exits, add_arc):
+ def process_return_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool:
for xit in exits:
add_arc(
xit.lineno, -self.start, xit.cause,
@@ -506,10 +550,9 @@
return True
-class TryBlock(BlockBase):
+class TryBlock(Block):
"""A block on the block stack representing a `try` block."""
- @contract(handler_start='int|None', final_start='int|None')
- def __init__(self, handler_start, final_start):
+ def __init__(self, handler_start: Optional[TLineNo], final_start: Optional[TLineNo]) -> None:
# The line number of the first "except" handler, if any.
self.handler_start = handler_start
# The line number of the "finally:" clause, if any.
@@ -517,24 +560,24 @@
# The ArcStarts for breaks/continues/returns/raises inside the "try:"
# that need to route through the "finally:" clause.
- self.break_from = set()
- self.continue_from = set()
- self.raise_from = set()
- self.return_from = set()
+ self.break_from: Set[ArcStart] = set()
+ self.continue_from: Set[ArcStart] = set()
+ self.raise_from: Set[ArcStart] = set()
+ self.return_from: Set[ArcStart] = set()
- def process_break_exits(self, exits, add_arc):
+ def process_break_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool:
if self.final_start is not None:
self.break_from.update(exits)
return True
return False
- def process_continue_exits(self, exits, add_arc):
+ def process_continue_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool:
if self.final_start is not None:
self.continue_from.update(exits)
return True
return False
- def process_raise_exits(self, exits, add_arc):
+ def process_raise_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool:
if self.handler_start is not None:
for xit in exits:
add_arc(xit.lineno, self.handler_start, xit.cause)
@@ -543,17 +586,16 @@
self.raise_from.update(exits)
return True
- def process_return_exits(self, exits, add_arc):
+ def process_return_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool:
if self.final_start is not None:
self.return_from.update(exits)
return True
return False
-class WithBlock(BlockBase):
+class WithBlock(Block):
"""A block on the block stack representing a `with` block."""
- @contract(start=int)
- def __init__(self, start):
+ 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.
assert env.PYBEHAVIOR.exit_through_with
@@ -563,11 +605,16 @@
# The ArcStarts for breaks/continues/returns/raises inside the "with:"
# that need to go through the with-statement while exiting.
- self.break_from = set()
- self.continue_from = set()
- self.return_from = set()
-
- def _process_exits(self, exits, add_arc, from_set=None):
+ self.break_from: Set[ArcStart] = set()
+ self.continue_from: Set[ArcStart] = set()
+ self.return_from: Set[ArcStart] = set()
+
+ def _process_exits(
+ self,
+ exits: Set[ArcStart],
+ add_arc: TAddArcFn,
+ from_set: Optional[Set[ArcStart]] = None,
+ ) -> bool:
"""Helper to process the four kinds of exits."""
for xit in exits:
add_arc(xit.lineno, self.start, xit.cause)
@@ -575,48 +622,27 @@
from_set.update(exits)
return True
- def process_break_exits(self, exits, add_arc):
+ def process_break_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool:
return self._process_exits(exits, add_arc, self.break_from)
- def process_continue_exits(self, exits, add_arc):
+ def process_continue_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool:
return self._process_exits(exits, add_arc, self.continue_from)
- def process_raise_exits(self, exits, add_arc):
+ def process_raise_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool:
return self._process_exits(exits, add_arc)
- def process_return_exits(self, exits, add_arc):
+ def process_return_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool:
return self._process_exits(exits, add_arc, self.return_from)
-class ArcStart(collections.namedtuple("Arc", "lineno, cause")):
- """The information needed to start an arc.
-
- `lineno` is the line number the arc starts from.
-
- `cause` is an English text fragment used as the `startmsg` for
- AstArcAnalyzer.missing_arc_fragments. It will be used to describe why an
- arc wasn't executed, so should fit well into a sentence of the form,
- "Line 17 didn't run because {cause}." The fragment can include "{lineno}"
- to have `lineno` interpolated into it.
-
- """
- def __new__(cls, lineno, cause=None):
- return super().__new__(cls, lineno, cause)
-
-
-# Define contract words that PyContract doesn't have.
-# ArcStarts is for a list or set of ArcStart's.
-new_contract('ArcStarts', lambda seq: all(isinstance(x, ArcStart) for x in seq))
-
-
-class NodeList:
+class NodeList(ast.AST):
"""A synthetic fictitious node, containing a sequence of nodes.
This is used when collapsing optimized if-statements, to represent the
unconditional execution of one of the clauses.
"""
- def __init__(self, body):
+ def __init__(self, body: Sequence[ast.AST]) -> None:
self.body = body
self.lineno = body[0].lineno
@@ -624,17 +650,25 @@
# TODO: the cause messages have too many commas.
# TODO: Shouldn't the cause messages join with "and" instead of "or"?
-def ast_parse(text):
- """How we create an AST parse."""
- return ast.parse(neuter_encoding_declaration(text))
+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}")
+ self.add_arc(start, -start, None, f"didn't finish the {noun} on line {start}")
+ return _code_object__expression_callable
class AstArcAnalyzer:
"""Analyze source text with an AST to find executable code paths."""
- @contract(text='unicode', statements=set)
- def __init__(self, text, statements, multiline):
- self.root_node = ast_parse(text)
+ def __init__(
+ self,
+ text: str,
+ statements: Set[TLineNo],
+ multiline: Dict[TLineNo, TLineNo],
+ ) -> None:
+ self.root_node = ast.parse(text)
# TODO: I think this is happening in too many places.
self.statements = {multiline.get(l, l) for l in statements}
self.multiline = multiline
@@ -649,20 +683,20 @@
print(f"Multiline map: {self.multiline}")
ast_dump(self.root_node)
- self.arcs = set()
+ self.arcs: Set[TArc] = set()
# A map from arc pairs to a list of pairs of sentence fragments:
# { (start, end): [(startmsg, endmsg), ...], }
#
# For an arc from line 17, they should be usable like:
# "Line 17 {endmsg}, because {startmsg}"
- self.missing_arc_fragments = collections.defaultdict(list)
- self.block_stack = []
+ self.missing_arc_fragments: TArcFragments = collections.defaultdict(list)
+ self.block_stack: List[Block] = []
# $set_env.py: COVERAGE_TRACK_ARCS - Trace possible arcs added while parsing code.
self.debug = bool(int(os.environ.get("COVERAGE_TRACK_ARCS", 0)))
- def analyze(self):
+ def analyze(self) -> None:
"""Examine the AST tree from `root_node` to determine possible arcs.
This sets the `arcs` attribute to be a set of (from, to) line number
@@ -675,8 +709,13 @@
if code_object_handler is not None:
code_object_handler(node)
- @contract(start=int, end=int)
- def add_arc(self, start, end, smsg=None, emsg=None):
+ def add_arc(
+ self,
+ start: TLineNo,
+ end: TLineNo,
+ smsg: Optional[str] = None,
+ emsg: Optional[str] = None,
+ ) -> 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}")
@@ -686,25 +725,27 @@
if smsg is not None or emsg is not None:
self.missing_arc_fragments[(start, end)].append((smsg, emsg))
- def nearest_blocks(self):
+ def nearest_blocks(self) -> Iterable[Block]:
"""Yield the blocks in nearest-to-farthest order."""
return reversed(self.block_stack)
- @contract(returns=int)
- def line_for_node(self, node):
+ def line_for_node(self, node: ast.AST) -> TLineNo:
"""What is the right line number to use for this node?
This dispatches to _line__Node functions where needed.
"""
node_name = node.__class__.__name__
- handler = getattr(self, "_line__" + node_name, None)
+ handler = cast(
+ Optional[Callable[[ast.AST], TLineNo]],
+ getattr(self, "_line__" + node_name, None)
+ )
if handler is not None:
return handler(node)
else:
return node.lineno
- def _line_decorated(self, node):
+ def _line_decorated(self, node: ast.FunctionDef) -> TLineNo:
"""Compute first line number for things that can be decorated (classes and functions)."""
lineno = node.lineno
if env.PYBEHAVIOR.trace_decorated_def or env.PYBEHAVIOR.def_ast_no_decorator:
@@ -712,17 +753,17 @@
lineno = node.decorator_list[0].lineno
return lineno
- def _line__Assign(self, node):
+ def _line__Assign(self, node: ast.Assign) -> TLineNo:
return self.line_for_node(node.value)
_line__ClassDef = _line_decorated
- def _line__Dict(self, node):
+ def _line__Dict(self, node: ast.Dict) -> TLineNo:
if node.keys:
if node.keys[0] is not None:
return node.keys[0].lineno
else:
- # Unpacked dict literals `{**{'a':1}}` have None as the key,
+ # Unpacked dict literals `{**{"a":1}}` have None as the key,
# use the value in that case.
return node.values[0].lineno
else:
@@ -731,13 +772,13 @@
_line__FunctionDef = _line_decorated
_line__AsyncFunctionDef = _line_decorated
- def _line__List(self, node):
+ def _line__List(self, node: ast.List) -> TLineNo:
if node.elts:
return self.line_for_node(node.elts[0])
else:
return node.lineno
- def _line__Module(self, node):
+ def _line__Module(self, node: ast.Module) -> TLineNo:
if env.PYBEHAVIOR.module_firstline_1:
return 1
elif node.body:
@@ -752,8 +793,7 @@
"Import", "ImportFrom", "Nonlocal", "Pass",
}
- @contract(returns='ArcStarts')
- def add_arcs(self, node):
+ def add_arcs(self, node: ast.AST) -> Set[ArcStart]:
"""Add the arcs for `node`.
Return a set of ArcStarts, exits from this node to the next. Because a
@@ -770,7 +810,10 @@
"""
node_name = node.__class__.__name__
- handler = getattr(self, "_handle__" + node_name, None)
+ handler = cast(
+ Optional[Callable[[ast.AST], Set[ArcStart]]],
+ getattr(self, "_handle__" + node_name, None)
+ )
if handler is not None:
return handler(node)
else:
@@ -778,14 +821,17 @@
# statement), or it's something we overlooked.
if env.TESTING:
if node_name not in self.OK_TO_DEFAULT:
- raise Exception(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))}
- @one_of("from_start, prev_starts")
- @contract(returns='ArcStarts')
- def add_body_arcs(self, body, from_start=None, prev_starts=None):
+ def add_body_arcs(
+ self,
+ body: Sequence[ast.AST],
+ from_start: Optional[ArcStart] = None,
+ prev_starts: Optional[Set[ArcStart]] = None
+ ) -> Set[ArcStart]:
"""Add arcs for the body of a compound statement.
`body` is the body node. `from_start` is a single `ArcStart` that can
@@ -797,21 +843,23 @@
"""
if prev_starts is None:
+ assert from_start is not None
prev_starts = {from_start}
for body_node in body:
lineno = self.line_for_node(body_node)
first_line = self.multiline.get(lineno, lineno)
if first_line not in self.statements:
- body_node = self.find_non_missing_node(body_node)
- if body_node is None:
+ maybe_body_node = self.find_non_missing_node(body_node)
+ if maybe_body_node is None:
continue
+ body_node = maybe_body_node
lineno = self.line_for_node(body_node)
for prev_start in prev_starts:
self.add_arc(prev_start.lineno, lineno, prev_start.cause)
prev_starts = self.add_arcs(body_node)
return prev_starts
- def find_non_missing_node(self, node):
+ def find_non_missing_node(self, node: ast.AST) -> Optional[ast.AST]:
"""Search `node` looking for a child that has not been optimized away.
This might return the node you started with, or it will work recursively
@@ -828,12 +876,15 @@
if first_line in self.statements:
return node
- missing_fn = getattr(self, "_missing__" + node.__class__.__name__, None)
- if missing_fn:
- node = missing_fn(node)
+ missing_fn = cast(
+ Optional[Callable[[ast.AST], Optional[ast.AST]]],
+ getattr(self, "_missing__" + node.__class__.__name__, None)
+ )
+ if missing_fn is not None:
+ ret_node = missing_fn(node)
else:
- node = None
- return node
+ ret_node = None
+ return ret_node
# Missing nodes: _missing__*
#
@@ -842,7 +893,7 @@
# find_non_missing_node) to find a node to use instead of the missing
# node. They can return None if the node should truly be gone.
- def _missing__If(self, node):
+ def _missing__If(self, node: ast.If) -> Optional[ast.AST]:
# If the if-node is missing, then one of its children might still be
# here, but not both. So return the first of the two that isn't missing.
# Use a NodeList to hold the clauses as a single node.
@@ -853,14 +904,14 @@
return self.find_non_missing_node(NodeList(node.orelse))
return None
- def _missing__NodeList(self, node):
+ def _missing__NodeList(self, node: NodeList) -> Optional[ast.AST]:
# A NodeList might be a mixture of missing and present nodes. Find the
# ones that are present.
non_missing_children = []
for child in node.body:
- child = self.find_non_missing_node(child)
- if child is not None:
- non_missing_children.append(child)
+ maybe_child = self.find_non_missing_node(child)
+ if maybe_child is not None:
+ non_missing_children.append(maybe_child)
# Return the simplest representation of the present children.
if not non_missing_children:
@@ -869,7 +920,7 @@
return non_missing_children[0]
return NodeList(non_missing_children)
- def _missing__While(self, node):
+ def _missing__While(self, node: ast.While) -> Optional[ast.AST]:
body_nodes = self.find_non_missing_node(NodeList(node.body))
if not body_nodes:
return None
@@ -879,16 +930,17 @@
new_while.test = ast.Name()
new_while.test.lineno = body_nodes.lineno
new_while.test.id = "True"
+ assert hasattr(body_nodes, "body")
new_while.body = body_nodes.body
- new_while.orelse = None
+ new_while.orelse = []
return new_while
- def is_constant_expr(self, node):
+ def is_constant_expr(self, node: ast.AST) -> Optional[str]:
"""Is this a compile-time constant?"""
node_name = node.__class__.__name__
if node_name in ["Constant", "NameConstant", "Num"]:
return "Num"
- elif node_name == "Name":
+ elif isinstance(node, ast.Name):
if node.id in ["True", "False", "None", "__debug__"]:
return "Name"
return None
@@ -900,7 +952,6 @@
# listcomps hidden in lists: x = [[i for i in range(10)]]
# nested function definitions
-
# Exit processing: process_*_exits
#
# These functions process the four kinds of jump exits: break, continue,
@@ -909,29 +960,25 @@
# enclosing loop block, or the nearest enclosing finally block, whichever
# is nearer.
- @contract(exits='ArcStarts')
- def process_break_exits(self, exits):
+ def process_break_exits(self, exits: Set[ArcStart]) -> None:
"""Add arcs due to jumps from `exits` being breaks."""
for block in self.nearest_blocks(): # pragma: always breaks
if block.process_break_exits(exits, self.add_arc):
break
- @contract(exits='ArcStarts')
- def process_continue_exits(self, exits):
+ def process_continue_exits(self, exits: Set[ArcStart]) -> None:
"""Add arcs due to jumps from `exits` being continues."""
for block in self.nearest_blocks(): # pragma: always breaks
if block.process_continue_exits(exits, self.add_arc):
break
- @contract(exits='ArcStarts')
- def process_raise_exits(self, exits):
+ def process_raise_exits(self, exits: Set[ArcStart]) -> None:
"""Add arcs due to jumps from `exits` being raises."""
for block in self.nearest_blocks():
if block.process_raise_exits(exits, self.add_arc):
break
- @contract(exits='ArcStarts')
- def process_return_exits(self, exits):
+ def process_return_exits(self, exits: Set[ArcStart]) -> None:
"""Add arcs due to jumps from `exits` being returns."""
for block in self.nearest_blocks(): # pragma: always breaks
if block.process_return_exits(exits, self.add_arc):
@@ -948,17 +995,16 @@
# Every node type that represents a statement should have a handler, or it
# should be listed in OK_TO_DEFAULT.
- @contract(returns='ArcStarts')
- def _handle__Break(self, node):
+ def _handle__Break(self, node: ast.Break) -> Set[ArcStart]:
here = self.line_for_node(node)
break_start = ArcStart(here, cause="the break on line {lineno} wasn't executed")
- self.process_break_exits([break_start])
+ self.process_break_exits({break_start})
return set()
- @contract(returns='ArcStarts')
- def _handle_decorated(self, node):
+ def _handle_decorated(self, node: ast.FunctionDef) -> Set[ArcStart]:
"""Add arcs for things that can be decorated (classes and functions)."""
- main_line = last = node.lineno
+ main_line: TLineNo = node.lineno
+ last: Optional[TLineNo] = node.lineno
decs = node.decorator_list
if decs:
if env.PYBEHAVIOR.trace_decorated_def or env.PYBEHAVIOR.def_ast_no_decorator:
@@ -968,6 +1014,7 @@
if last is not None and dec_start != last:
self.add_arc(last, dec_start)
last = dec_start
+ assert last is not None
if env.PYBEHAVIOR.trace_decorated_def:
self.add_arc(last, main_line)
last = main_line
@@ -988,19 +1035,18 @@
self.add_arc(last, lineno)
last = lineno
# The body is handled in collect_arcs.
+ assert last is not None
return {ArcStart(last)}
_handle__ClassDef = _handle_decorated
- @contract(returns='ArcStarts')
- def _handle__Continue(self, node):
+ def _handle__Continue(self, node: ast.Continue) -> Set[ArcStart]:
here = self.line_for_node(node)
continue_start = ArcStart(here, cause="the continue on line {lineno} wasn't executed")
- self.process_continue_exits([continue_start])
+ self.process_continue_exits({continue_start})
return set()
- @contract(returns='ArcStarts')
- def _handle__For(self, node):
+ 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")
@@ -1009,6 +1055,7 @@
for xit in exits:
self.add_arc(xit.lineno, start, xit.cause)
my_block = self.block_stack.pop()
+ assert isinstance(my_block, LoopBlock)
exits = my_block.break_exits
from_start = ArcStart(start, cause="the loop on line {lineno} didn't complete")
if node.orelse:
@@ -1024,8 +1071,7 @@
_handle__FunctionDef = _handle_decorated
_handle__AsyncFunctionDef = _handle_decorated
- @contract(returns='ArcStarts')
- def _handle__If(self, node):
+ 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")
exits = self.add_body_arcs(node.body, from_start=from_start)
@@ -1033,48 +1079,50 @@
exits |= self.add_body_arcs(node.orelse, from_start=from_start)
return exits
- @contract(returns='ArcStarts')
- def _handle__Match(self, node):
- start = self.line_for_node(node)
- last_start = start
- exits = set()
- had_wildcard = False
- for case in node.cases:
- case_start = self.line_for_node(case.pattern)
- if isinstance(case.pattern, ast.MatchAs):
- had_wildcard = True
- 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")
- exits |= self.add_body_arcs(case.body, from_start=from_start)
- last_start = case_start
- if not had_wildcard:
- exits.add(from_start)
- return exits
+ if sys.version_info >= (3, 10):
+ def _handle__Match(self, node: ast.Match) -> Set[ArcStart]:
+ start = self.line_for_node(node)
+ last_start = start
+ exits = set()
+ had_wildcard = False
+ for case in node.cases:
+ case_start = self.line_for_node(case.pattern)
+ pattern = case.pattern
+ while isinstance(pattern, ast.MatchOr):
+ 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")
+ from_start = ArcStart(
+ case_start,
+ 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(from_start)
+ return exits
- @contract(returns='ArcStarts')
- def _handle__NodeList(self, node):
+ def _handle__NodeList(self, node: NodeList) -> Set[ArcStart]:
start = self.line_for_node(node)
exits = self.add_body_arcs(node.body, from_start=ArcStart(start))
return exits
- @contract(returns='ArcStarts')
- def _handle__Raise(self, node):
+ def _handle__Raise(self, node: ast.Raise) -> Set[ArcStart]:
here = self.line_for_node(node)
raise_start = ArcStart(here, cause="the raise on line {lineno} wasn't executed")
- self.process_raise_exits([raise_start])
+ self.process_raise_exits({raise_start})
# `raise` statement jumps away, no exits from here.
return set()
- @contract(returns='ArcStarts')
- def _handle__Return(self, node):
+ def _handle__Return(self, node: ast.Return) -> Set[ArcStart]:
here = self.line_for_node(node)
return_start = ArcStart(here, cause="the return on line {lineno} wasn't executed")
- self.process_return_exits([return_start])
+ self.process_return_exits({return_start})
# `return` statement jumps away, no exits from here.
return set()
- @contract(returns='ArcStarts')
- def _handle__Try(self, node):
+ def _handle__Try(self, node: ast.Try) -> Set[ArcStart]:
if node.handlers:
handler_start = self.line_for_node(node.handlers[0])
else:
@@ -1107,10 +1155,10 @@
else:
self.block_stack.pop()
- handler_exits = set()
+ handler_exits: Set[ArcStart] = set()
if node.handlers:
- last_handler_start = None
+ last_handler_start: Optional[TLineNo] = None
for handler_node in node.handlers:
handler_start = self.line_for_node(handler_node)
if last_handler_start is not None:
@@ -1185,8 +1233,7 @@
return exits
- @contract(starts='ArcStarts', exits='ArcStarts', returns='ArcStarts')
- def _combine_finally_starts(self, starts, exits):
+ def _combine_finally_starts(self, starts: Set[ArcStart], exits: Set[ArcStart]) -> Set[ArcStart]:
"""Helper for building the cause of `finally` branches.
"finally" clauses might not execute their exits, and the causes could
@@ -1201,8 +1248,7 @@
exits = {ArcStart(xit.lineno, cause) for xit in exits}
return exits
- @contract(returns='ArcStarts')
- def _handle__While(self, node):
+ def _handle__While(self, node: ast.While) -> Set[ArcStart]:
start = to_top = self.line_for_node(node.test)
constant_test = self.is_constant_expr(node.test)
top_is_body0 = False
@@ -1219,6 +1265,7 @@
self.add_arc(xit.lineno, to_top, xit.cause)
exits = set()
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")
if node.orelse:
@@ -1230,14 +1277,14 @@
exits.add(from_start)
return exits
- @contract(returns='ArcStarts')
- def _handle__With(self, node):
+ def _handle__With(self, node: ast.With) -> Set[ArcStart]:
start = self.line_for_node(node)
if env.PYBEHAVIOR.exit_through_with:
self.block_stack.append(WithBlock(start=start))
exits = self.add_body_arcs(node.body, from_start=ArcStart(start))
if env.PYBEHAVIOR.exit_through_with:
with_block = self.block_stack.pop()
+ assert isinstance(with_block, WithBlock)
with_exit = {ArcStart(start)}
if exits:
for xit in exits:
@@ -1264,7 +1311,7 @@
# These methods are used by analyze() as the start of the analysis.
# There is one for each construct with a code object.
- def _code_object__Module(self, node):
+ def _code_object__Module(self, node: ast.Module) -> None:
start = self.line_for_node(node)
if node.body:
exits = self.add_body_arcs(node.body, from_start=ArcStart(-start))
@@ -1275,7 +1322,7 @@
self.add_arc(-start, start)
self.add_arc(start, -start)
- def _code_object__FunctionDef(self, node):
+ def _code_object__FunctionDef(self, node: ast.FunctionDef) -> None:
start = self.line_for_node(node)
self.block_stack.append(FunctionBlock(start=start, name=node.name))
exits = self.add_body_arcs(node.body, from_start=ArcStart(-start))
@@ -1284,7 +1331,7 @@
_code_object__AsyncFunctionDef = _code_object__FunctionDef
- def _code_object__ClassDef(self, node):
+ def _code_object__ClassDef(self, node: ast.ClassDef) -> None:
start = self.line_for_node(node)
self.add_arc(-start, start)
exits = self.add_body_arcs(node.body, from_start=ArcStart(start))
@@ -1294,33 +1341,30 @@
f"didn't exit the body of class {node.name!r}",
)
- def _make_expression_code_method(noun): # pylint: disable=no-self-argument
- """A function to make methods for expression-based callable _code_object__ methods."""
- def _code_object__expression_callable(self, node):
- start = self.line_for_node(node)
- self.add_arc(-start, start, None, f"didn't run the {noun} on line {start}")
- self.add_arc(start, -start, None, f"didn't finish the {noun} on line {start}")
- return _code_object__expression_callable
-
_code_object__Lambda = _make_expression_code_method("lambda")
_code_object__GeneratorExp = _make_expression_code_method("generator expression")
- _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")
+ 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 only used when dumping the AST for debugging.
SKIP_DUMP_FIELDS = ["ctx"]
-def _is_simple_value(value):
+def _is_simple_value(value: Any) -> bool:
"""Is `value` simple enough to be displayed on a single line?"""
return (
- value in [None, [], (), {}, set()] or
+ value in [None, [], (), {}, set(), frozenset(), Ellipsis] or
isinstance(value, (bytes, int, float, str))
)
-def ast_dump(node, depth=0, print=print): # pylint: disable=redefined-builtin
+def ast_dump(
+ node: ast.AST,
+ depth: int = 0,
+ print: Callable[[str], None] = print, # pylint: disable=redefined-builtin
+) -> None:
"""Dump the AST for `node`.
This recursively walks the AST, printing a readable version.
@@ -1331,6 +1375,7 @@
if lineno is not None:
linemark = f" @ {node.lineno},{node.col_offset}"
if hasattr(node, "end_lineno"):
+ assert hasattr(node, "end_col_offset")
linemark += ":"
if node.end_lineno != node.lineno:
linemark += f"{node.end_lineno},"
@@ -1352,7 +1397,7 @@
else:
print(head)
if 0:
- print("{}# mro: {}".format(
+ print("{}# mro: {}".format( # type: ignore[unreachable]
indent, ", ".join(c.__name__ for c in node.__class__.__mro__[1:]),
))
next_indent = indent + " "
diff -Nru python-coverage-6.5.0+dfsg1/coverage/phystokens.py python-coverage-7.2.7+dfsg1/coverage/phystokens.py
--- python-coverage-6.5.0+dfsg1/coverage/phystokens.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/phystokens.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,17 +3,26 @@
"""Better tokenizing for coverage.py."""
+from __future__ import annotations
+
import ast
+import io
import keyword
import re
+import sys
import token
import tokenize
+from typing import Iterable, List, Optional, Set, Tuple
+
from coverage import env
-from coverage.misc import contract
+from coverage.types import TLineNo, TSourceTokenLines
+
+TokenInfos = Iterable[tokenize.TokenInfo]
-def phys_tokens(toks):
+
+def _phys_tokens(toks: TokenInfos) -> TokenInfos:
"""Return all physical tokens, even line continuations.
tokenize.generate_tokens() doesn't return a token for the backslash that
@@ -23,9 +32,9 @@
Returns the same values as generate_tokens()
"""
- last_line = None
+ last_line: Optional[str] = None
last_lineno = -1
- last_ttext = None
+ 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"):
@@ -48,7 +57,7 @@
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
@@ -56,7 +65,7 @@
# Figure out what column the backslash is in.
ccol = len(last_line.split("\n")[-2]) - 1
# Yield the token, with a fake token type.
- yield (
+ yield tokenize.TokenInfo(
99999, "\\\n",
(slineno, ccol), (slineno, ccol+2),
last_line
@@ -64,27 +73,27 @@
last_line = ltext
if ttype not in (tokenize.NEWLINE, tokenize.NL):
last_ttext = ttext
- yield ttype, ttext, (slineno, scol), (elineno, ecol), ltext
+ yield tokenize.TokenInfo(ttype, ttext, (slineno, scol), (elineno, ecol), ltext)
last_lineno = elineno
class MatchCaseFinder(ast.NodeVisitor):
"""Helper for finding match/case lines."""
- def __init__(self, source):
+ def __init__(self, source: str) -> None:
# This will be the set of line numbers that start match or case statements.
- self.match_case_lines = set()
+ self.match_case_lines: Set[TLineNo] = set()
self.visit(ast.parse(source))
- def visit_Match(self, node):
- """Invoked by ast.NodeVisitor.visit"""
- self.match_case_lines.add(node.lineno)
- for case in node.cases:
- self.match_case_lines.add(case.pattern.lineno)
- self.generic_visit(node)
+ if sys.version_info >= (3, 10):
+ def visit_Match(self, node: ast.Match) -> None:
+ """Invoked by ast.NodeVisitor.visit"""
+ self.match_case_lines.add(node.lineno)
+ for case in node.cases:
+ self.match_case_lines.add(case.pattern.lineno)
+ self.generic_visit(node)
-@contract(source='unicode')
-def source_token_lines(source):
+def source_token_lines(source: str) -> TSourceTokenLines:
"""Generate a series of lines, one for each line in `source`.
Each line is a list of pairs, each pair is a token::
@@ -95,30 +104,30 @@
If you concatenate all the token texts, and then join them with newlines,
you should have your original `source` back, with two differences:
- trailing whitespace is not preserved, and a final line with no newline
+ trailing white space is not preserved, and a final line with no newline
is indistinguishable from a final line with a newline.
"""
ws_tokens = {token.INDENT, token.DEDENT, token.NEWLINE, tokenize.NL}
- line = []
+ 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:
match_case_lines = MatchCaseFinder(source).match_case_lines
- for ttype, ttext, (sline, scol), (_, ecol), _ in phys_tokens(tokgen):
+ 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
@@ -126,22 +135,25 @@
if mark_start and 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"
- elif env.PYBEHAVIOR.soft_keywords and keyword.issoftkeyword(ttext):
- # Soft keywords appear at the start of the line, 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":
- is_start_of_line = True
- else:
- is_start_of_line = False
- if is_start_of_line and sline in match_case_lines:
- tok_class = "key"
+ elif sys.version_info >= (3, 10): # PYVERSIONS
+ # Need the version_info check to keep mypy from borking
+ # on issoftkeyword here.
+ if env.PYBEHAVIOR.soft_keywords and keyword.issoftkeyword(ttext):
+ # Soft keywords appear at the start of the line,
+ # 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":
+ is_start_of_line = True
+ else:
+ is_start_of_line = False
+ if is_start_of_line and sline in match_case_lines:
+ tok_class = "key"
line.append((tok_class, part))
mark_end = True
scol = 0
@@ -163,16 +175,15 @@
actually tokenize twice.
"""
- def __init__(self):
- self.last_text = None
- self.last_tokens = None
+ def __init__(self) -> None:
+ self.last_text: Optional[str] = None
+ self.last_tokens: List[tokenize.TokenInfo] = []
- @contract(text='unicode')
- def generate_tokens(self, text):
+ def generate_tokens(self, text: str) -> TokenInfos:
"""A stand-in for `tokenize.generate_tokens`."""
if text != self.last_text:
self.last_text = text
- readline = iter(text.splitlines(True)).__next__
+ readline = io.StringIO(text).readline
try:
self.last_tokens = list(tokenize.generate_tokens(readline))
except:
@@ -184,10 +195,7 @@
generate_tokens = CachedTokenizer().generate_tokens
-COOKIE_RE = re.compile(r"^[ \t]*#.*coding[:=][ \t]*([-\w.]+)", flags=re.MULTILINE)
-
-@contract(source='bytes')
-def source_encoding(source):
+def source_encoding(source: bytes) -> str:
"""Determine the encoding for `source`, according to PEP 263.
`source` is a byte string: the text of the program.
@@ -197,31 +205,3 @@
"""
readline = iter(source.splitlines(True)).__next__
return tokenize.detect_encoding(readline)[0]
-
-
-@contract(source='unicode')
-def compile_unicode(source, filename, mode):
- """Just like the `compile` builtin, but works on any Unicode string.
-
- Python 2's compile() builtin has a stupid restriction: if the source string
- is Unicode, then it may not have a encoding declaration in it. Why not?
- Who knows! It also decodes to utf-8, and then tries to interpret those
- utf-8 bytes according to the encoding declaration. Why? Who knows!
-
- This function neuters the coding declaration, and compiles it.
-
- """
- source = neuter_encoding_declaration(source)
- code = compile(source, filename, mode)
- return code
-
-
-@contract(source='unicode', returns='unicode')
-def neuter_encoding_declaration(source):
- """Return `source`, with any encoding declaration neutered."""
- if COOKIE_RE.search(source):
- source_lines = source.splitlines(True)
- for lineno in range(min(2, len(source_lines))):
- source_lines[lineno] = COOKIE_RE.sub("# (deleted declaration)", source_lines[lineno])
- source = "".join(source_lines)
- return source
diff -Nru python-coverage-6.5.0+dfsg1/coverage/plugin.py python-coverage-7.2.7+dfsg1/coverage/plugin.py
--- python-coverage-6.5.0+dfsg1/coverage/plugin.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/plugin.py 2023-05-29 19:46:30.000000000 +0000
@@ -112,16 +112,25 @@
"""
+from __future__ import annotations
+
import functools
+from types import FrameType
+from typing import Any, Dict, Iterable, Optional, Set, Tuple, Union
+
from coverage import files
-from coverage.misc import contract, _needs_to_implement
+from coverage.misc import _needs_to_implement
+from coverage.types import TArc, TConfigurable, TLineNo, TSourceTokenLines
class CoveragePlugin:
"""Base class for coverage.py plug-ins."""
- def file_tracer(self, filename): # pylint: disable=unused-argument
+ _coverage_plugin_name: str
+ _coverage_enabled: bool
+
+ def file_tracer(self, filename: str) -> Optional[FileTracer]: # pylint: disable=unused-argument
"""Get a :class:`FileTracer` object for a file.
Plug-in type: file tracer.
@@ -161,7 +170,10 @@
"""
return None
- def file_reporter(self, filename): # pylint: disable=unused-argument
+ def file_reporter( # type: ignore[return]
+ self,
+ filename: str, # pylint: disable=unused-argument
+ ) -> Union[FileReporter, str]: # str should be Literal["python"]
"""Get the :class:`FileReporter` class to use for a file.
Plug-in type: file tracer.
@@ -175,7 +187,10 @@
"""
_needs_to_implement(self, "file_reporter")
- def dynamic_context(self, frame): # pylint: disable=unused-argument
+ def dynamic_context(
+ self,
+ frame: FrameType, # pylint: disable=unused-argument
+ ) -> Optional[str]:
"""Get the dynamically computed context label for `frame`.
Plug-in type: dynamic context.
@@ -191,7 +206,10 @@
"""
return None
- def find_executable_files(self, src_dir): # pylint: disable=unused-argument
+ def find_executable_files(
+ self,
+ src_dir: str, # pylint: disable=unused-argument
+ ) -> Iterable[str]:
"""Yield all of the executable files in `src_dir`, recursively.
Plug-in type: file tracer.
@@ -206,7 +224,7 @@
"""
return []
- def configure(self, config):
+ def configure(self, config: TConfigurable) -> None:
"""Modify the configuration of coverage.py.
Plug-in type: configurer.
@@ -220,7 +238,7 @@
"""
pass
- def sys_info(self):
+ def sys_info(self) -> Iterable[Tuple[str, Any]]:
"""Get a list of information useful for debugging.
Plug-in type: any.
@@ -234,7 +252,12 @@
return []
-class FileTracer:
+class CoveragePluginBase:
+ """Plugins produce specialized objects, which point back to the original plugin."""
+ _coverage_plugin: CoveragePlugin
+
+
+class FileTracer(CoveragePluginBase):
"""Support needed for files during the execution phase.
File tracer plug-ins implement subclasses of FileTracer to return from
@@ -251,7 +274,7 @@
"""
- def source_filename(self):
+ def source_filename(self) -> str: # type: ignore[return]
"""The source file name for this file.
This may be any file name you like. A key responsibility of a plug-in
@@ -266,7 +289,7 @@
"""
_needs_to_implement(self, "source_filename")
- def has_dynamic_source_filename(self):
+ def has_dynamic_source_filename(self) -> bool:
"""Does this FileTracer have dynamic source file names?
FileTracers can provide dynamically determined file names by
@@ -284,7 +307,11 @@
"""
return False
- def dynamic_source_filename(self, filename, frame): # pylint: disable=unused-argument
+ def dynamic_source_filename(
+ self,
+ filename: str, # pylint: disable=unused-argument
+ frame: FrameType, # pylint: disable=unused-argument
+ ) -> Optional[str]:
"""Get a dynamically computed source file name.
Some plug-ins need to compute the source file name dynamically for each
@@ -299,7 +326,7 @@
"""
return None
- def line_number_range(self, frame):
+ def line_number_range(self, frame: FrameType) -> Tuple[TLineNo, TLineNo]:
"""Get the range of source line numbers for a given a call frame.
The call frame is examined, and the source line number in the original
@@ -318,7 +345,7 @@
@functools.total_ordering
-class FileReporter:
+class FileReporter(CoveragePluginBase):
"""Support needed for files during the analysis and reporting phases.
File tracer plug-ins implement a subclass of `FileReporter`, and return
@@ -331,7 +358,7 @@
"""
- def __init__(self, filename):
+ def __init__(self, filename: str) -> None:
"""Simple initialization of a `FileReporter`.
The `filename` argument is the path to the file being reported. This
@@ -341,10 +368,10 @@
"""
self.filename = filename
- def __repr__(self):
+ def __repr__(self) -> str:
return "<{0.__class__.__name__} filename={0.filename!r}>".format(self)
- def relative_filename(self):
+ def relative_filename(self) -> str:
"""Get the relative file name for this file.
This file path will be displayed in reports. The default
@@ -355,8 +382,7 @@
"""
return files.relative_filename(self.filename)
- @contract(returns='unicode')
- def source(self):
+ def source(self) -> str:
"""Get the source for the file.
Returns a Unicode string.
@@ -366,10 +392,10 @@
as a text file, or if you need other encoding support.
"""
- with open(self.filename, "rb") as f:
- return f.read().decode("utf-8")
+ with open(self.filename, encoding="utf-8") as f:
+ return f.read()
- def lines(self):
+ def lines(self) -> Set[TLineNo]: # type: ignore[return]
"""Get the executable lines in this file.
Your plug-in must determine which lines in the file were possibly
@@ -380,7 +406,7 @@
"""
_needs_to_implement(self, "lines")
- def excluded_lines(self):
+ def excluded_lines(self) -> Set[TLineNo]:
"""Get the excluded executable lines in this file.
Your plug-in can use any method it likes to allow the user to exclude
@@ -393,7 +419,7 @@
"""
return set()
- def translate_lines(self, lines):
+ def translate_lines(self, lines: Iterable[TLineNo]) -> Set[TLineNo]:
"""Translate recorded lines into reported lines.
Some file formats will want to report lines slightly differently than
@@ -413,7 +439,7 @@
"""
return set(lines)
- def arcs(self):
+ def arcs(self) -> Set[TArc]:
"""Get the executable arcs in this file.
To support branch coverage, your plug-in needs to be able to indicate
@@ -427,7 +453,7 @@
"""
return set()
- def no_branch_lines(self):
+ def no_branch_lines(self) -> Set[TLineNo]:
"""Get the lines excused from branch coverage in this file.
Your plug-in can use any method it likes to allow the user to exclude
@@ -440,7 +466,7 @@
"""
return set()
- def translate_arcs(self, arcs):
+ def translate_arcs(self, arcs: Iterable[TArc]) -> Set[TArc]:
"""Translate recorded arcs into reported arcs.
Similar to :meth:`translate_lines`, but for arcs. `arcs` is a set of
@@ -451,9 +477,9 @@
The default implementation returns `arcs` unchanged.
"""
- return arcs
+ return set(arcs)
- def exit_counts(self):
+ def exit_counts(self) -> Dict[TLineNo, int]:
"""Get a count of exits from that each line.
To determine which lines are branches, coverage.py looks for lines that
@@ -466,7 +492,12 @@
"""
return {}
- def missing_arc_description(self, start, end, executed_arcs=None): # pylint: disable=unused-argument
+ def missing_arc_description(
+ self,
+ start: TLineNo,
+ end: TLineNo,
+ executed_arcs: Optional[Iterable[TArc]] = None, # pylint: disable=unused-argument
+ ) -> str:
"""Provide an English sentence describing a missing arc.
The `start` and `end` arguments are the line numbers of the missing
@@ -481,41 +512,42 @@
"""
return f"Line {start} didn't jump to line {end}"
- def source_token_lines(self):
+ def source_token_lines(self) -> TSourceTokenLines:
"""Generate a series of tokenized lines, one for each line in `source`.
These tokens are used for syntax-colored reports.
Each line is a list of pairs, each pair is a token::
- [('key', 'def'), ('ws', ' '), ('nam', 'hello'), ('op', '('), ... ]
+ [("key", "def"), ("ws", " "), ("nam", "hello"), ("op", "("), ... ]
Each pair has a token class, and the token text. The token classes
are:
- * ``'com'``: a comment
- * ``'key'``: a keyword
- * ``'nam'``: a name, or identifier
- * ``'num'``: a number
- * ``'op'``: an operator
- * ``'str'``: a string literal
- * ``'ws'``: some white space
- * ``'txt'``: some other kind of text
+ * ``"com"``: a comment
+ * ``"key"``: a keyword
+ * ``"nam"``: a name, or identifier
+ * ``"num"``: a number
+ * ``"op"``: an operator
+ * ``"str"``: a string literal
+ * ``"ws"``: some white space
+ * ``"txt"``: some other kind of text
If you concatenate all the token texts, and then join them with
newlines, you should have your original source back.
The default implementation simply returns each line tagged as
- ``'txt'``.
+ ``"txt"``.
"""
for line in self.source().splitlines():
- yield [('txt', line)]
+ yield [("txt", line)]
- def __eq__(self, other):
+ def __eq__(self, other: Any) -> bool:
return isinstance(other, FileReporter) and self.filename == other.filename
- def __lt__(self, other):
+ def __lt__(self, other: Any) -> bool:
return isinstance(other, FileReporter) and self.filename < other.filename
- __hash__ = None # This object doesn't need to be hashed.
+ # This object doesn't need to be hashed.
+ __hash__ = None # type: ignore[assignment]
diff -Nru python-coverage-6.5.0+dfsg1/coverage/plugin_support.py python-coverage-7.2.7+dfsg1/coverage/plugin_support.py
--- python-coverage-6.5.0+dfsg1/coverage/plugin_support.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/plugin_support.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,13 +3,21 @@
"""Support for plugins."""
+from __future__ import annotations
+
import os
import os.path
import sys
+from types import FrameType
+from typing import Any, Dict, Iterable, Iterator, List, Optional, Set, Tuple, Union
+
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,
+)
os = isolate_module(os)
@@ -17,18 +25,23 @@
class Plugins:
"""The currently loaded collection of coverage.py plugins."""
- def __init__(self):
- self.order = []
- self.names = {}
- self.file_tracers = []
- self.configurers = []
- self.context_switchers = []
+ def __init__(self) -> None:
+ self.order: List[CoveragePlugin] = []
+ self.names: Dict[str, CoveragePlugin] = {}
+ self.file_tracers: List[CoveragePlugin] = []
+ self.configurers: List[CoveragePlugin] = []
+ self.context_switchers: List[CoveragePlugin] = []
- self.current_module = None
- self.debug = None
+ self.current_module: Optional[str] = None
+ self.debug: Optional[TDebugCtl]
@classmethod
- def load_plugins(cls, modules, config, debug=None):
+ def load_plugins(
+ cls,
+ modules: Iterable[str],
+ config: TPluginConfig,
+ debug: Optional[TDebugCtl] = None,
+ ) -> Plugins:
"""Load plugins from `modules`.
Returns a Plugins object with the loaded and configured plugins.
@@ -54,7 +67,7 @@
plugins.current_module = None
return plugins
- def add_file_tracer(self, plugin):
+ def add_file_tracer(self, plugin: CoveragePlugin) -> None:
"""Add a file tracer plugin.
`plugin` is an instance of a third-party plugin class. It must
@@ -63,7 +76,7 @@
"""
self._add_plugin(plugin, self.file_tracers)
- def add_configurer(self, plugin):
+ def add_configurer(self, plugin: CoveragePlugin) -> None:
"""Add a configuring plugin.
`plugin` is an instance of a third-party plugin class. It must
@@ -72,7 +85,7 @@
"""
self._add_plugin(plugin, self.configurers)
- def add_dynamic_context(self, plugin):
+ def add_dynamic_context(self, plugin: CoveragePlugin) -> None:
"""Add a dynamic context plugin.
`plugin` is an instance of a third-party plugin class. It must
@@ -81,7 +94,7 @@
"""
self._add_plugin(plugin, self.context_switchers)
- def add_noop(self, plugin):
+ def add_noop(self, plugin: CoveragePlugin) -> None:
"""Add a plugin that does nothing.
This is only useful for testing the plugin support.
@@ -89,7 +102,11 @@
"""
self._add_plugin(plugin, None)
- def _add_plugin(self, plugin, specialized):
+ def _add_plugin(
+ self,
+ plugin: CoveragePlugin,
+ specialized: Optional[List[CoveragePlugin]],
+ ) -> None:
"""Add a plugin object.
`plugin` is a :class:`CoveragePlugin` instance to add. `specialized`
@@ -97,12 +114,11 @@
"""
plugin_name = f"{self.current_module}.{plugin.__class__.__name__}"
- if self.debug and self.debug.should('plugin'):
+ 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)
- # pylint: disable=attribute-defined-outside-init
plugin._coverage_plugin_name = plugin_name
plugin._coverage_enabled = True
self.order.append(plugin)
@@ -110,13 +126,13 @@
if specialized is not None:
specialized.append(plugin)
- def __bool__(self):
+ def __bool__(self) -> bool:
return bool(self.order)
- def __iter__(self):
+ def __iter__(self) -> Iterator[CoveragePlugin]:
return iter(self.order)
- def get(self, plugin_name):
+ def get(self, plugin_name: str) -> CoveragePlugin:
"""Return a plugin by name."""
return self.names[plugin_name]
@@ -124,20 +140,20 @@
class LabelledDebug:
"""A Debug writer, but with labels for prepending to the messages."""
- def __init__(self, label, debug, prev_labels=()):
+ def __init__(self, label: str, debug: TDebugCtl, prev_labels: Iterable[str] = ()):
self.labels = list(prev_labels) + [label]
self.debug = debug
- def add_label(self, label):
+ def add_label(self, label: str) -> LabelledDebug:
"""Add a label to the writer, and return a new `LabelledDebug`."""
return LabelledDebug(label, self.debug, self.labels)
- def message_prefix(self):
+ def message_prefix(self) -> str:
"""The prefix to use on messages, combining the labels."""
- prefixes = self.labels + ['']
+ prefixes = self.labels + [""]
return ":\n".join(" "*i+label for i, label in enumerate(prefixes))
- def write(self, message):
+ def write(self, message: str) -> None:
"""Write `message`, but with the labels prepended."""
self.debug.write(f"{self.message_prefix()}{message}")
@@ -145,12 +161,12 @@
class DebugPluginWrapper(CoveragePlugin):
"""Wrap a plugin, and use debug to report on what it's doing."""
- def __init__(self, plugin, debug):
+ def __init__(self, plugin: CoveragePlugin, debug: LabelledDebug) -> None:
super().__init__()
self.plugin = plugin
self.debug = debug
- def file_tracer(self, filename):
+ def file_tracer(self, filename: str) -> Optional[FileTracer]:
tracer = self.plugin.file_tracer(filename)
self.debug.write(f"file_tracer({filename!r}) --> {tracer!r}")
if tracer:
@@ -158,64 +174,65 @@
tracer = DebugFileTracerWrapper(tracer, debug)
return tracer
- def file_reporter(self, filename):
+ def file_reporter(self, filename: str) -> Union[FileReporter, str]:
reporter = self.plugin.file_reporter(filename)
+ assert isinstance(reporter, FileReporter)
self.debug.write(f"file_reporter({filename!r}) --> {reporter!r}")
if reporter:
debug = self.debug.add_label(f"file {filename!r}")
reporter = DebugFileReporterWrapper(filename, reporter, debug)
return reporter
- def dynamic_context(self, frame):
+ def dynamic_context(self, frame: FrameType) -> Optional[str]:
context = self.plugin.dynamic_context(frame)
self.debug.write(f"dynamic_context({frame!r}) --> {context!r}")
return context
- def find_executable_files(self, src_dir):
+ 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}")
return executable_files
- def configure(self, config):
+ def configure(self, config: TConfigurable) -> None:
self.debug.write(f"configure({config!r})")
self.plugin.configure(config)
- def sys_info(self):
+ def sys_info(self) -> Iterable[Tuple[str, Any]]:
return self.plugin.sys_info()
class DebugFileTracerWrapper(FileTracer):
"""A debugging `FileTracer`."""
- def __init__(self, tracer, debug):
+ def __init__(self, tracer: FileTracer, debug: LabelledDebug) -> None:
self.tracer = tracer
self.debug = debug
- def _show_frame(self, frame):
+ def _show_frame(self, frame: FrameType) -> str:
"""A short string identifying a frame, for debug messages."""
return "%s@%d" % (
os.path.basename(frame.f_code.co_filename),
frame.f_lineno,
)
- def source_filename(self):
+ def source_filename(self) -> str:
sfilename = self.tracer.source_filename()
self.debug.write(f"source_filename() --> {sfilename!r}")
return sfilename
- def has_dynamic_source_filename(self):
+ def has_dynamic_source_filename(self) -> bool:
has = self.tracer.has_dynamic_source_filename()
self.debug.write(f"has_dynamic_source_filename() --> {has!r}")
return has
- def dynamic_source_filename(self, filename, frame):
+ def dynamic_source_filename(self, filename: str, frame: FrameType) -> Optional[str]:
dyn = self.tracer.dynamic_source_filename(filename, frame)
self.debug.write("dynamic_source_filename({!r}, {}) --> {!r}".format(
filename, self._show_frame(frame), dyn,
))
return dyn
- def line_number_range(self, frame):
+ 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}")
return pair
@@ -224,57 +241,57 @@
class DebugFileReporterWrapper(FileReporter):
"""A debugging `FileReporter`."""
- def __init__(self, filename, reporter, debug):
+ def __init__(self, filename: str, reporter: FileReporter, debug: LabelledDebug) -> None:
super().__init__(filename)
self.reporter = reporter
self.debug = debug
- def relative_filename(self):
+ def relative_filename(self) -> str:
ret = self.reporter.relative_filename()
self.debug.write(f"relative_filename() --> {ret!r}")
return ret
- def lines(self):
+ def lines(self) -> Set[TLineNo]:
ret = self.reporter.lines()
self.debug.write(f"lines() --> {ret!r}")
return ret
- def excluded_lines(self):
+ def excluded_lines(self) -> Set[TLineNo]:
ret = self.reporter.excluded_lines()
self.debug.write(f"excluded_lines() --> {ret!r}")
return ret
- def translate_lines(self, lines):
+ 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}")
return ret
- def translate_arcs(self, arcs):
+ 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}")
return ret
- def no_branch_lines(self):
+ def no_branch_lines(self) -> Set[TLineNo]:
ret = self.reporter.no_branch_lines()
self.debug.write(f"no_branch_lines() --> {ret!r}")
return ret
- def exit_counts(self):
+ def exit_counts(self) -> Dict[TLineNo, int]:
ret = self.reporter.exit_counts()
self.debug.write(f"exit_counts() --> {ret!r}")
return ret
- def arcs(self):
+ def arcs(self) -> Set[TArc]:
ret = self.reporter.arcs()
self.debug.write(f"arcs() --> {ret!r}")
return ret
- def source(self):
+ def source(self) -> str:
ret = self.reporter.source()
self.debug.write("source() --> %d chars" % (len(ret),))
return ret
- def source_token_lines(self):
+ def source_token_lines(self) -> TSourceTokenLines:
ret = list(self.reporter.source_token_lines())
self.debug.write("source_token_lines() --> %d tokens" % (len(ret),))
return ret
diff -Nru python-coverage-6.5.0+dfsg1/coverage/python.py python-coverage-7.2.7+dfsg1/coverage/python.py
--- python-coverage-6.5.0+dfsg1/coverage/python.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/python.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,23 +3,30 @@
"""Python source expertise for coverage.py"""
+from __future__ import annotations
+
import os.path
import types
import zipimport
+from typing import Dict, Iterable, Optional, Set, TYPE_CHECKING
+
from coverage import env
from coverage.exceptions import CoverageException, NoSource
-from coverage.files import canonical_filename, relative_filename
-from coverage.misc import contract, expensive, isolate_module, join_regex
+from coverage.files import canonical_filename, relative_filename, zip_location
+from coverage.misc import expensive, isolate_module, join_regex
from coverage.parser import PythonParser
from coverage.phystokens import source_token_lines, source_encoding
from coverage.plugin import FileReporter
+from coverage.types import TArc, TLineNo, TMorf, TSourceTokenLines
+
+if TYPE_CHECKING:
+ from coverage import Coverage
os = isolate_module(os)
-@contract(returns='bytes')
-def read_python_source(filename):
+def read_python_source(filename: str) -> bytes:
"""Read the Python source text from `filename`.
Returns bytes.
@@ -28,15 +35,10 @@
with open(filename, "rb") as f:
source = f.read()
- if env.IRONPYTHON:
- # IronPython reads Unicode strings even for "rb" files.
- source = bytes(source)
-
return source.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
-@contract(returns='unicode')
-def get_python_source(filename):
+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:
@@ -44,34 +46,34 @@
else:
exts = [ext]
+ source_bytes: Optional[bytes]
for ext in exts:
try_filename = base + ext
if os.path.exists(try_filename):
# A regular text file: open it.
- source = read_python_source(try_filename)
+ source_bytes = read_python_source(try_filename)
break
# Maybe it's in a zip file?
- source = get_zip_bytes(try_filename)
- if source is not None:
+ source_bytes = get_zip_bytes(try_filename)
+ if source_bytes is not None:
break
else:
# Couldn't find source.
raise NoSource(f"No source for code: '{filename}'.")
# Replace \f because of http://bugs.python.org/issue19035
- source = source.replace(b'\f', b' ')
- source = source.decode(source_encoding(source), "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
-@contract(returns='bytes|None')
-def get_zip_bytes(filename):
+def get_zip_bytes(filename: str) -> Optional[bytes]:
"""Get data from `filename` if it is a zip file path.
Returns the bytestring data read from the zip file, or None if no zip file
@@ -79,23 +81,22 @@
an empty string if the file is empty.
"""
- markers = ['.zip'+os.sep, '.egg'+os.sep, '.pex'+os.sep]
- for marker in markers:
- if marker in filename:
- parts = filename.split(marker)
- try:
- zi = zipimport.zipimporter(parts[0]+marker[:-1])
- except zipimport.ZipImportError:
- continue
- try:
- data = zi.get_data(parts[1])
- except OSError:
- continue
- return data
+ zipfile_inner = zip_location(filename)
+ if zipfile_inner is not None:
+ zipfile, inner = zipfile_inner
+ try:
+ zi = zipimport.zipimporter(zipfile)
+ except zipimport.ZipImportError:
+ return None
+ try:
+ data = zi.get_data(inner)
+ except OSError:
+ return None
+ return data
return None
-def source_for_file(filename):
+def source_for_file(filename: str) -> str:
"""Return the source filename for `filename`.
Given a file name being traced, return the best guess as to the source
@@ -120,17 +121,13 @@
# Didn't find source, but it's probably the .py file we want.
return py_filename
- elif filename.endswith("$py.class"):
- # Jython is easy to guess.
- return filename[:-9] + ".py"
-
# No idea, just use the file name as-is.
return filename
-def source_for_morf(morf):
+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.
@@ -146,60 +143,68 @@
class PythonFileReporter(FileReporter):
"""Report support for a Python file."""
- def __init__(self, morf, coverage=None):
+ def __init__(self, morf: TMorf, coverage: Optional[Coverage] = None) -> None:
self.coverage = coverage
filename = source_for_morf(morf)
- super().__init__(canonical_filename(filename))
+ fname = filename
+ canonicalize = True
+ if self.coverage is not None:
+ if self.coverage.config.relative_files:
+ canonicalize = False
+ if canonicalize:
+ fname = canonical_filename(filename)
+ super().__init__(fname)
- if hasattr(morf, '__name__'):
+ if hasattr(morf, "__name__"):
name = morf.__name__.replace(".", os.sep)
- if os.path.basename(filename).startswith('__init__.'):
+ if os.path.basename(filename).startswith("__init__."):
name += os.sep + "__init__"
name += ".py"
else:
name = relative_filename(filename)
self.relname = name
- self._source = None
- self._parser = None
+ self._source: Optional[str] = None
+ self._parser: Optional[PythonParser] = None
self._excluded = None
- def __repr__(self):
+ def __repr__(self) -> str:
return f""
- @contract(returns='unicode')
- def relative_filename(self):
+ def relative_filename(self) -> str:
return self.relname
@property
- def parser(self):
+ def parser(self) -> PythonParser:
"""Lazily create a :class:`PythonParser`."""
+ assert self.coverage is not None
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
- def lines(self):
+ def lines(self) -> Set[TLineNo]:
"""Return the line numbers of statements in the file."""
return self.parser.statements
- def excluded_lines(self):
+ def excluded_lines(self) -> Set[TLineNo]:
"""Return the line numbers of statements in the file."""
return self.parser.excluded
- def translate_lines(self, lines):
+ def translate_lines(self, lines: Iterable[TLineNo]) -> Set[TLineNo]:
return self.parser.translate_lines(lines)
- def translate_arcs(self, arcs):
+ def translate_arcs(self, arcs: Iterable[TArc]) -> Set[TArc]:
return self.parser.translate_arcs(arcs)
@expensive
- def no_branch_lines(self):
+ def no_branch_lines(self) -> Set[TLineNo]:
+ assert self.coverage is not None
no_branch = self.parser.lines_matching(
join_regex(self.coverage.config.partial_list),
join_regex(self.coverage.config.partial_always_list),
@@ -207,23 +212,27 @@
return no_branch
@expensive
- def arcs(self):
+ def arcs(self) -> Set[TArc]:
return self.parser.arcs()
@expensive
- def exit_counts(self):
+ def exit_counts(self) -> Dict[TLineNo, int]:
return self.parser.exit_counts()
- def missing_arc_description(self, start, end, executed_arcs=None):
+ def missing_arc_description(
+ self,
+ start: TLineNo,
+ end: TLineNo,
+ executed_arcs: Optional[Iterable[TArc]] = None,
+ ) -> str:
return self.parser.missing_arc_description(start, end, executed_arcs)
- @contract(returns='unicode')
- def source(self):
+ def source(self) -> str:
if self._source is None:
self._source = get_python_source(self.filename)
return self._source
- def should_be_python(self):
+ def should_be_python(self) -> bool:
"""Does it seem like this file should contain Python?
This is used to decide if a file reported as part of the execution of
@@ -235,7 +244,7 @@
_, 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:
@@ -243,5 +252,5 @@
# Everything else is probably not Python.
return False
- def source_token_lines(self):
+ def source_token_lines(self) -> TSourceTokenLines:
return source_token_lines(self.source())
diff -Nru python-coverage-6.5.0+dfsg1/coverage/pytracer.py python-coverage-7.2.7+dfsg1/coverage/pytracer.py
--- python-coverage-6.5.0+dfsg1/coverage/pytracer.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/pytracer.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,18 +3,28 @@
"""Raw data collector for coverage.py."""
+from __future__ import annotations
+
import atexit
import dis
import sys
+import threading
+
+from types import FrameType, ModuleType
+from typing import Any, Callable, Dict, List, Optional, Set, Tuple, cast
from coverage import env
+from coverage.types import (
+ TArc, TFileDisposition, TLineNo, TTraceData, TTraceFileData, TTraceFn,
+ TTracer, TWarnFn,
+)
# We need the YIELD_VALUE opcode below, in a comparison-friendly form.
-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
@@ -22,7 +32,7 @@
THIS_FILE = __file__.rstrip("co")
-class PyTracer:
+class PyTracer(TTracer):
"""Python implementation of the raw data tracer."""
# Because of poor implementations of trace-function-manipulating tools,
@@ -41,44 +51,46 @@
# PyTracer to get accurate results. The command-line --timid argument is
# used to force the use of this tracer.
- def __init__(self):
+ def __init__(self) -> None:
+ # pylint: disable=super-init-not-called
# Attributes set from the collector:
- self.data = None
+ self.data: TTraceData
self.trace_arcs = False
- self.should_trace = None
- self.should_trace_cache = None
- self.should_start_context = None
- self.warn = None
+ self.should_trace: Callable[[str, FrameType], TFileDisposition]
+ self.should_trace_cache: Dict[str, Optional[TFileDisposition]]
+ self.should_start_context: Optional[Callable[[FrameType], Optional[str]]] = None
+ self.switch_context: Optional[Callable[[Optional[str]], None]] = None
+ self.warn: TWarnFn
+
# The threading module to use, if any.
- self.threading = None
+ self.threading: Optional[ModuleType] = None
- self.cur_file_data = None
- self.last_line = 0 # int, but uninitialized.
- self.cur_file_name = None
- self.context = None
+ self.cur_file_data: Optional[TTraceFileData] = None
+ self.last_line: TLineNo = 0
+ self.cur_file_name: Optional[str] = None
+ self.context: Optional[str] = None
self.started_context = False
- self.data_stack = []
- self.thread = None
+ self.data_stack: List[Tuple[Optional[TTraceFileData], Optional[str], TLineNo, bool]] = []
+ self.thread: Optional[threading.Thread] = None
self.stopped = False
self._activity = False
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.
- self._cached_bound_method_trace = self._trace
+ self._cached_bound_method_trace: TTraceFn = self._trace
- def __repr__(self):
- return "".format(
- id(self),
- sum(len(v) for v in self.data.values()),
- len(self.data),
- )
+ def __repr__(self) -> str:
+ me = id(self)
+ points = sum(len(v) for v in self.data.values())
+ files = len(self.data)
+ return f""
- def log(self, marker, *args):
+ 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("{} {}[{}]".format(
@@ -87,13 +99,13 @@
len(self.data_stack),
))
if 0: # if you want thread ids..
- f.write(".{:x}.{:x}".format(
+ 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(" | ")
+ f.write(" | ") # type: ignore[unreachable]
stack = " / ".join(
(fname or "???").rpartition("/")[-1]
for _, fname, _, _ in self.data_stack
@@ -101,7 +113,13 @@
f.write(stack)
f.write("\n")
- def _trace(self, frame, event, arg_unused):
+ def _trace(
+ self,
+ frame: FrameType,
+ event: str,
+ arg: Any, # pylint: disable=unused-argument
+ lineno: Optional[TLineNo] = None, # pylint: disable=unused-argument
+ ) -> Optional[TTraceFn]:
"""The trace function passed to sys.settrace."""
if THIS_FILE in frame.f_code.co_filename:
@@ -113,27 +131,36 @@
# The PyTrace.stop() method has been called, possibly by another
# thread, let's deactivate ourselves now.
if 0:
- self.log("---\nX", frame.f_code.co_filename, frame.f_lineno)
- f = frame
+ f = frame # type: ignore[unreachable]
+ 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)
f = f.f_back
sys.settrace(None)
- self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = (
- self.data_stack.pop()
- )
+ try:
+ self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = (
+ self.data_stack.pop()
+ )
+ except IndexError:
+ self.log(
+ "Empty stack!",
+ frame.f_code.co_filename,
+ frame.f_lineno,
+ frame.f_code.co_name
+ )
return None
- # if event != 'call' and frame.f_code.co_filename != self.cur_file_name:
+ # 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)
if context_maybe is not None:
self.context = context_maybe
started_context = True
+ assert self.switch_context is not None
self.switch_context(self.context)
else:
started_context = False
@@ -155,7 +182,7 @@
# Improve tracing performance: when calling a function, both caller
# and callee are often within the same file. if that's the case, we
# don't have to re-check whether to trace the corresponding
- # function (which is a little bit espensive since it involves
+ # function (which is a little bit expensive since it involves
# dictionary lookups). This optimization is only correct if we
# didn't start a context.
filename = frame.f_code.co_filename
@@ -169,8 +196,9 @@
self.cur_file_data = None
if disp.trace:
tracename = disp.source_filename
+ assert tracename is not None
if tracename not in self.data:
- self.data[tracename] = set()
+ self.data[tracename] = set() # type: ignore[assignment]
self.cur_file_data = self.data[tracename]
else:
frame.f_trace_lines = False
@@ -187,24 +215,24 @@
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:
- lineno = frame.f_lineno
+ flineno: TLineNo = frame.f_lineno
if self.trace_arcs:
- self.cur_file_data.add((self.last_line, lineno))
+ cast(Set[TArc], self.cur_file_data).add((self.last_line, flineno))
else:
- self.cur_file_data.add(lineno)
- self.last_line = lineno
+ 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.
@@ -230,7 +258,7 @@
real_return = True
if real_return:
first = frame.f_code.co_firstlineno
- self.cur_file_data.add((self.last_line, -first))
+ cast(Set[TArc], self.cur_file_data).add((self.last_line, -first))
# Leaving this function, pop the filename stack.
self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = (
@@ -238,11 +266,12 @@
)
# Leaving a context?
if self.started_context:
+ assert self.switch_context is not None
self.context = None
self.switch_context(None)
return self._cached_bound_method_trace
- def start(self):
+ def start(self) -> TTraceFn:
"""Start this Tracer.
Return a Python function suitable for use with sys.settrace().
@@ -263,7 +292,7 @@
sys.settrace(self._cached_bound_method_trace)
return self._cached_bound_method_trace
- def stop(self):
+ def stop(self) -> None:
"""Stop this Tracer."""
# Get the active tracer callback before setting the stop flag to be
# able to detect if the tracer was changed prior to stopping it.
@@ -274,12 +303,14 @@
# right thread.
self.stopped = True
- if self.threading and self.thread.ident != self.threading.current_thread().ident:
- # Called on a different thread than started us: we can't unhook
- # ourselves, but we've set the flag that we should stop, so we
- # won't do any more tracing.
- #self.log("~", "stopping on different threads")
- return
+ if self.threading:
+ assert self.thread is not None
+ if self.thread.ident != self.threading.current_thread().ident:
+ # Called on a different thread than started us: we can't unhook
+ # ourselves, but we've set the flag that we should stop, so we
+ # won't do any more tracing.
+ #self.log("~", "stopping on different threads")
+ return
if self.warn:
# PyPy clears the trace function before running atexit functions,
@@ -293,14 +324,14 @@
slug="trace-changed",
)
- def activity(self):
+ def activity(self) -> bool:
"""Has there been any activity?"""
return self._activity
- def reset_activity(self):
+ def reset_activity(self) -> None:
"""Reset the activity() flag."""
self._activity = False
- def get_stats(self):
+ def get_stats(self) -> Optional[Dict[str, int]]:
"""Return a dictionary of statistics, or None."""
return None
diff -Nru python-coverage-6.5.0+dfsg1/coverage/py.typed python-coverage-7.2.7+dfsg1/coverage/py.typed
--- python-coverage-6.5.0+dfsg1/coverage/py.typed 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/py.typed 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +1 @@
+# Marker file for PEP 561 to indicate that this package has type hints.
diff -Nru python-coverage-6.5.0+dfsg1/coverage/report_core.py python-coverage-7.2.7+dfsg1/coverage/report_core.py
--- python-coverage-6.5.0+dfsg1/coverage/report_core.py 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/report_core.py 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +1,117 @@
+# 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, Iterable, Iterator, IO, Optional, Tuple, 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.plugin import FileReporter
+from coverage.results import Analysis
+from coverage.types import Protocol, TMorf
+
+if TYPE_CHECKING:
+ from coverage import Coverage
+
+
+class Reporter(Protocol):
+ """What we expect of reporters."""
+
+ report_type: str
+
+ def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float:
+ """Generate a report of `morfs`, written to `outfile`."""
+
+
+def render_report(
+ output_path: str,
+ reporter: Reporter,
+ morfs: Optional[Iterable[TMorf]],
+ msgfn: Callable[[str], None],
+) -> float:
+ """Run a one-file report generator, managing the output file.
+
+ This function ensures the output file is ready to be written to. Then writes
+ the report to it. Then closes the file and cleans up.
+
+ """
+ file_to_close = None
+ delete_file = False
+
+ 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")
+ 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}")
+ delete_file = False
+ return ret
+ finally:
+ if file_to_close is not None:
+ file_to_close.close()
+ if delete_file:
+ file_be_gone(output_path) # pragma: part covered (doesn't return)
+
+
+def get_analysis_to_report(
+ coverage: Coverage,
+ morfs: Optional[Iterable[TMorf]],
+) -> Iterator[Tuple[FileReporter, Analysis]]:
+ """Get the files to report on.
+
+ For each morf in `morfs`, if it should be reported on (based on the omit
+ and include configuration options), yield a pair, the `FileReporter` and
+ `Analysis` for the morf.
+
+ """
+ file_reporters = coverage._get_file_reporters(morfs)
+ config = coverage.config
+
+ if config.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")
+ file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)]
+
+ if not file_reporters:
+ raise NoDataError("No data to report.")
+
+ for fr in sorted(file_reporters):
+ try:
+ analysis = coverage._analyze(fr)
+ except NotPython:
+ # Only report errors for .py files, and only if we didn't
+ # explicitly suppress those errors.
+ # NotPython is only raised by PythonFileReporter, which has a
+ # should_be_python() method.
+ 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")
+ 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")
+ else:
+ raise
+ else:
+ yield (fr, analysis)
diff -Nru python-coverage-6.5.0+dfsg1/coverage/report.py python-coverage-7.2.7+dfsg1/coverage/report.py
--- python-coverage-6.5.0+dfsg1/coverage/report.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/report.py 2023-05-29 19:46:30.000000000 +0000
@@ -1,91 +1,281 @@
# 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."""
+"""Summary reporting"""
+
+from __future__ import annotations
import sys
-from coverage.exceptions import CoverageException, NoDataError, NotPython
-from coverage.files import prep_patterns, FnmatchMatcher
-from coverage.misc import ensure_dir_for_file, file_be_gone
-
-
-def render_report(output_path, reporter, morfs, msgfn):
- """Run a one-file report generator, managing the output file.
-
- This function ensures the output file is ready to be written to. Then writes
- the report to it. Then closes the file and cleans up.
-
- """
- file_to_close = None
- delete_file = False
-
- if output_path == "-":
- outfile = sys.stdout
- else:
- # Ensure that the output directory is created; done here
- # because this report pre-opens the output file.
- # HTMLReport does this using the Report plumbing because
- # its task is more complex, being multiple files.
- ensure_dir_for_file(output_path)
- outfile = open(output_path, "w", encoding="utf-8")
- file_to_close = outfile
-
- try:
- return reporter.report(morfs, outfile=outfile)
- except CoverageException:
- delete_file = True
- raise
- finally:
- if file_to_close:
- file_to_close.close()
- if delete_file:
- file_be_gone(output_path) # pragma: part covered (doesn't return)
+from typing import Any, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING
+
+from coverage.exceptions import ConfigError, 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.types import TMorf
+
+if TYPE_CHECKING:
+ from coverage import Coverage
+
+
+class SummaryReporter:
+ """A reporter for writing the summary report."""
+
+ def __init__(self, coverage: Coverage) -> None:
+ self.coverage = coverage
+ self.config = self.coverage.config
+ self.branches = coverage.get_data().has_arcs()
+ self.outfile: Optional[IO[str]] = 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.fr_analysis: List[Tuple[FileReporter, Analysis]] = []
+ self.skipped_count = 0
+ self.empty_count = 0
+ self.total = Numbers(precision=self.config.precision)
+
+ def write(self, line: str) -> None:
+ """Write a line to the output, adding a newline."""
+ assert self.outfile is not None
+ self.outfile.write(line.rstrip())
+ self.outfile.write("\n")
+
+ def write_items(self, items: Iterable[str]) -> None:
+ """Write a list of strings, joined together."""
+ self.write("".join(items))
+
+ def _report_text(
+ self,
+ header: List[str],
+ lines_values: List[List[Any]],
+ total_line: List[Any],
+ end_lines: List[str],
+ ) -> None:
+ """Internal method that prints report data in text format.
+
+ `header` is a list with captions.
+ `lines_values` is list of lists of sortable values.
+ `total_line` is a list with values of the total line.
+ `end_lines` is a list of ending lines with information about skipped files.
+
+ """
+ # 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])
+ formats = dict(
+ 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)
+
+ # Write the header
+ self.write(header_str)
+ self.write(rule)
+
+ 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)
+ ]
+ self.write_items(line_items)
+
+ # Write a TOTAL line
+ if lines_values:
+ 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)
+ ]
+ self.write_items(line_items)
+
+ for end_line in end_lines:
+ self.write(end_line)
+
+ def _report_markdown(
+ self,
+ header: List[str],
+ lines_values: List[List[Any]],
+ total_line: List[Any],
+ end_lines: List[str],
+ ) -> None:
+ """Internal method that prints report data in markdown format.
+
+ `header` is a list with captions.
+ `lines_values` is a sorted list of lists containing coverage information.
+ `total_line` is a list with values of the total line.
+ `end_lines` is a list of ending lines with information about skipped files.
+
+ """
+ # 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
+ formats = dict(
+ 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 "))
+ 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:]]
+ )
+
+ # Write the header
+ self.write(header_str)
+ self.write(rule_str)
+
+ for values in lines_values:
+ # build string with line values
+ formats.update(dict(Cover="{:>{n}}% |"))
+ line_items = [
+ 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}} |"))
+ total_line_items: List[str] = []
+ for item, value in zip(header, total_line):
+ if value == "":
+ insert = value
+ elif item == "Cover":
+ insert = f" **{value}%**"
else:
- msgfn(f"Wrote {reporter.report_type} to {output_path}")
+ 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:
+ self.write(end_line)
+ def report(self, morfs: Optional[Iterable[TMorf]], outfile: Optional[IO[str]] = None) -> float:
+ """Writes a report summarizing coverage statistics per module.
-def get_analysis_to_report(coverage, morfs):
- """Get the files to report on.
+ `outfile` is a text-mode file object to write the summary to.
- For each morf in `morfs`, if it should be reported on (based on the omit
- and include configuration options), yield a pair, the `FileReporter` and
- `Analysis` for the morf.
-
- """
- file_reporters = coverage._get_file_reporters(morfs)
- config = coverage.config
-
- if config.report_include:
- matcher = FnmatchMatcher(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 = FnmatchMatcher(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.")
-
- for fr in sorted(file_reporters):
- try:
- analysis = coverage._analyze(fr)
- except NotPython:
- # Only report errors for .py files, and only if we didn't
- # explicitly suppress those errors.
- # NotPython is only raised by PythonFileReporter, which has a
- # should_be_python() method.
- if fr.should_be_python():
- if config.ignore_errors:
- msg = f"Couldn't parse Python file '{fr.filename}'"
- 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")
- else:
- raise
+ """
+ self.outfile = outfile or sys.stdout
+
+ self.coverage.get_data().set_query_contexts(self.config.report_contexts)
+ for fr, analysis in get_analysis_to_report(self.coverage, morfs):
+ self.report_one_file(fr, analysis)
+
+ if not self.total.n_files and not self.skipped_count:
+ raise NoDataError("No data to report.")
+
+ if self.output_format == "total":
+ self.write(self.total.pc_covered_str)
+ else:
+ self.tabular_report()
+
+ return self.total.pc_covered
+
+ def tabular_report(self) -> None:
+ """Writes tabular report formats."""
+ # Prepare the header line and column sorting.
+ header = ["Name", "Stmts", "Miss"]
+ if self.branches:
+ header += ["Branch", "BrPart"]
+ header += ["Cover"]
+ if self.config.show_missing:
+ header += ["Missing"]
+
+ column_order = dict(name=0, stmts=1, miss=2, cover=-1)
+ if self.branches:
+ column_order.update(dict(branch=3, brpart=4))
+
+ # `lines_values` is list of lists of sortable values.
+ lines_values = []
+
+ for (fr, analysis) in self.fr_analysis:
+ nums = analysis.numbers
+
+ args = [fr.relative_filename(), nums.n_statements, nums.n_missing]
+ if self.branches:
+ args += [nums.n_branches, nums.n_partial_branches]
+ args += [nums.pc_covered_str]
+ if self.config.show_missing:
+ args += [analysis.missing_formatted(branches=True)]
+ args += [nums.pc_covered]
+ lines_values.append(args)
+
+ # Line sorting.
+ sort_option = (self.config.sort or "name").lower()
+ reverse = False
+ if sort_option[0] == "-":
+ reverse = True
+ sort_option = sort_option[1:]
+ 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":
+ lines_values = human_sorted_items(lines_values, reverse=reverse)
+ else:
+ lines_values.sort(
+ key=lambda line: (line[sort_idx], line[0]), # type: ignore[index]
+ reverse=reverse,
+ )
+
+ # Calculate total if we had at least one file.
+ 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 += [""]
+
+ # Create other final lines.
+ end_lines = []
+ if self.config.skip_covered and self.skipped_count:
+ 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."
+ )
+ 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.")
+
+ if self.output_format == "markdown":
+ formatter = self._report_markdown
+ else:
+ formatter = self._report_text
+ formatter(header, lines_values, total_line, end_lines)
+
+ def report_one_file(self, fr: FileReporter, analysis: Analysis) -> None:
+ """Report on just one file, the callback from report()."""
+ nums = analysis.numbers
+ self.total += nums
+
+ no_missing_lines = (nums.n_missing == 0)
+ no_missing_branches = (nums.n_partial_branches == 0)
+ if self.config.skip_covered and no_missing_lines and no_missing_branches:
+ # Don't report on 100% files.
+ self.skipped_count += 1
+ elif self.config.skip_empty and nums.n_statements == 0:
+ # Don't report on empty files.
+ self.empty_count += 1
else:
- yield (fr, analysis)
+ self.fr_analysis.append((fr, analysis))
diff -Nru python-coverage-6.5.0+dfsg1/coverage/results.py python-coverage-7.2.7+dfsg1/coverage/results.py
--- python-coverage-6.5.0+dfsg1/coverage/results.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/results.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,17 +3,32 @@
"""Results of coverage measurement."""
+from __future__ import annotations
+
import collections
-from coverage.debug import SimpleReprMixin
+from typing import Callable, Dict, Iterable, List, Optional, Tuple, TYPE_CHECKING
+
+from coverage.debug import AutoReprMixin
from coverage.exceptions import ConfigError
-from coverage.misc import contract, nice_pair
+from coverage.misc import nice_pair
+from coverage.types import TArc, TLineNo
+
+if TYPE_CHECKING:
+ from coverage.data import CoverageData
+ from coverage.plugin import FileReporter
class Analysis:
"""The results of analyzing a FileReporter."""
- def __init__(self, data, precision, file_reporter, file_mapper):
+ def __init__(
+ self,
+ data: CoverageData,
+ precision: int,
+ file_reporter: FileReporter,
+ file_mapper: Callable[[str], str],
+ ) -> None:
self.data = data
self.file_reporter = file_reporter
self.filename = file_mapper(self.file_reporter.filename)
@@ -21,6 +36,7 @@
self.excluded = self.file_reporter.excluded_lines()
# Identify missing statements.
+ executed: Iterable[TLineNo]
executed = self.data.lines(self.filename) or []
executed = self.file_reporter.translate_lines(executed)
self.executed = executed
@@ -51,7 +67,7 @@
n_missing_branches=n_missing_branches,
)
- def missing_formatted(self, branches=False):
+ def missing_formatted(self, branches: bool = False) -> str:
"""The missing line numbers, formatted nicely.
Returns a string like "1-2, 5-11, 13-14".
@@ -66,25 +82,23 @@
return format_lines(self.statements, self.missing, arcs=arcs)
- def has_arcs(self):
+ def has_arcs(self) -> bool:
"""Were arcs measured in this result?"""
return self.data.has_arcs()
- @contract(returns='list(tuple(int, int))')
- def arc_possibilities(self):
+ def arc_possibilities(self) -> List[TArc]:
"""Returns a sorted list of the arcs in the code."""
return self._arc_possibilities
- @contract(returns='list(tuple(int, int))')
- def arcs_executed(self):
+ def arcs_executed(self) -> List[TArc]:
"""Returns a sorted list of the arcs actually executed in the code."""
+ executed: Iterable[TArc]
executed = self.data.arcs(self.filename) or []
executed = self.file_reporter.translate_arcs(executed)
return sorted(executed)
- @contract(returns='list(tuple(int, int))')
- def arcs_missing(self):
- """Returns a sorted list of the unexecuted arcs in the code."""
+ def arcs_missing(self) -> List[TArc]:
+ """Returns a sorted list of the un-executed arcs in the code."""
possible = self.arc_possibilities()
executed = self.arcs_executed()
missing = (
@@ -95,8 +109,7 @@
)
return sorted(missing)
- @contract(returns='list(tuple(int, int))')
- def arcs_unpredicted(self):
+ def arcs_unpredicted(self) -> List[TArc]:
"""Returns a sorted list of the executed arcs missing from the code."""
possible = self.arc_possibilities()
executed = self.arcs_executed()
@@ -113,16 +126,15 @@
)
return sorted(unpredicted)
- def _branch_lines(self):
+ 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]
- def _total_branches(self):
+ def _total_branches(self) -> int:
"""How many total branches are there?"""
return sum(count for count in self.exit_counts.values() if count > 1)
- @contract(returns='dict(int: list(int))')
- def missing_branch_arcs(self):
+ def missing_branch_arcs(self) -> Dict[TLineNo, List[TLineNo]]:
"""Return arcs that weren't executed from branch lines.
Returns {l1:[l2a,l2b,...], ...}
@@ -136,8 +148,7 @@
mba[l1].append(l2)
return mba
- @contract(returns='dict(int: list(int))')
- def executed_branch_arcs(self):
+ def executed_branch_arcs(self) -> Dict[TLineNo, List[TLineNo]]:
"""Return arcs that were executed from branch lines.
Returns {l1:[l2a,l2b,...], ...}
@@ -151,8 +162,7 @@
eba[l1].append(l2)
return eba
- @contract(returns='dict(int: tuple(int, int))')
- def branch_stats(self):
+ def branch_stats(self) -> Dict[TLineNo, Tuple[int, int]]:
"""Get stats about branches.
Returns a dict mapping line numbers to a tuple:
@@ -168,7 +178,7 @@
return stats
-class Numbers(SimpleReprMixin):
+class Numbers(AutoReprMixin):
"""The numerical results of measuring coverage.
This holds the basic statistics from `Analysis`, and is used to roll
@@ -176,11 +186,17 @@
"""
- def __init__(self,
- precision=0,
- n_files=0, n_statements=0, n_excluded=0, n_missing=0,
- n_branches=0, n_partial_branches=0, n_missing_branches=0
- ):
+ def __init__(
+ self,
+ precision: int = 0,
+ n_files: int = 0,
+ n_statements: int = 0,
+ n_excluded: int = 0,
+ n_missing: int = 0,
+ n_branches: int = 0,
+ n_partial_branches: int = 0,
+ n_missing_branches: int = 0,
+ ) -> None:
assert 0 <= precision < 10
self._precision = precision
self._near0 = 1.0 / 10**precision
@@ -193,7 +209,7 @@
self.n_partial_branches = n_partial_branches
self.n_missing_branches = n_missing_branches
- def init_args(self):
+ def init_args(self) -> List[int]:
"""Return a list for __init__(*args) to recreate this object."""
return [
self._precision,
@@ -202,17 +218,17 @@
]
@property
- def n_executed(self):
+ def n_executed(self) -> int:
"""Returns the number of executed statements."""
return self.n_statements - self.n_missing
@property
- def n_executed_branches(self):
+ def n_executed_branches(self) -> int:
"""Returns the number of executed branches."""
return self.n_branches - self.n_missing_branches
@property
- def pc_covered(self):
+ def pc_covered(self) -> float:
"""Returns a single percentage value for coverage."""
if self.n_statements > 0:
numerator, denominator = self.ratio_covered
@@ -222,7 +238,7 @@
return pc_cov
@property
- def pc_covered_str(self):
+ def pc_covered_str(self) -> str:
"""Returns the percent covered, as a string, without a percent sign.
Note that "0" is only returned when the value is truly zero, and "100"
@@ -232,7 +248,7 @@
"""
return self.display_covered(self.pc_covered)
- def display_covered(self, pc):
+ def display_covered(self, pc: float) -> str:
"""Return a displayable total percentage, as a string.
Note that "0" is only returned when the value is truly zero, and "100"
@@ -248,7 +264,7 @@
pc = round(pc, self._precision)
return "%.*f" % (self._precision, pc)
- def pc_str_width(self):
+ def pc_str_width(self) -> int:
"""How many characters wide can pc_covered_str be?"""
width = 3 # "100"
if self._precision > 0:
@@ -256,13 +272,13 @@
return width
@property
- def ratio_covered(self):
+ def ratio_covered(self) -> Tuple[int, int]:
"""Return a numerator and denominator for the coverage ratio."""
numerator = self.n_executed + self.n_executed_branches
denominator = self.n_statements + self.n_branches
return numerator, denominator
- def __add__(self, other):
+ def __add__(self, other: Numbers) -> Numbers:
nums = Numbers(precision=self._precision)
nums.n_files = self.n_files + other.n_files
nums.n_statements = self.n_statements + other.n_statements
@@ -277,13 +293,16 @@
)
return nums
- def __radd__(self, other):
+ def __radd__(self, other: int) -> Numbers:
# Implementing 0+Numbers allows us to sum() a list of Numbers.
assert other == 0 # we only ever call it this way.
return self
-def _line_ranges(statements, lines):
+def _line_ranges(
+ statements: Iterable[TLineNo],
+ lines: Iterable[TLineNo],
+) -> List[Tuple[TLineNo, TLineNo]]:
"""Produce a list of ranges for `format_lines`."""
statements = sorted(statements)
lines = sorted(lines)
@@ -307,7 +326,11 @@
return pairs
-def format_lines(statements, lines, arcs=None):
+def format_lines(
+ statements: Iterable[TLineNo],
+ lines: Iterable[TLineNo],
+ arcs: Optional[Iterable[Tuple[TLineNo, List[TLineNo]]]] = None,
+) -> str:
"""Nicely format a list of line numbers.
Format a list of line numbers for printing by coalescing groups of lines as
@@ -326,7 +349,7 @@
"""
line_items = [(pair[0], nice_pair(pair)) for pair in _line_ranges(statements, lines)]
- if arcs:
+ if arcs is not None:
line_exits = sorted(arcs)
for line, exits in line_exits:
for ex in sorted(exits):
@@ -334,12 +357,11 @@
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
-@contract(total='number', fail_under='number', precision=int, returns=bool)
-def should_fail_under(total, fail_under, precision):
+def should_fail_under(total: float, fail_under: float, precision: int) -> bool:
"""Determine if a total should fail due to fail-under.
`total` is a float, the coverage measurement total. `fail_under` is the
diff -Nru python-coverage-6.5.0+dfsg1/coverage/sqldata.py python-coverage-7.2.7+dfsg1/coverage/sqldata.py
--- python-coverage-6.5.0+dfsg1/coverage/sqldata.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/sqldata.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,7 +3,10 @@
"""SQLite coverage data."""
+from __future__ import annotations
+
import collections
+import contextlib
import datetime
import functools
import glob
@@ -18,16 +21,22 @@
import threading
import zlib
-from coverage.debug import NoDebugging, SimpleReprMixin, clipped_repr
+from typing import (
+ cast, Any, Callable, Collection, Dict, Iterable, Iterator, List, Mapping,
+ Optional, Sequence, Set, Tuple, TypeVar, Union,
+)
+
+from coverage.debug import NoDebugging, AutoReprMixin, clipped_repr
from coverage.exceptions import CoverageException, DataError
from coverage.files import PathAliases
-from coverage.misc import contract, file_be_gone, isolate_module
+from coverage.misc import file_be_gone, isolate_module
from coverage.numbits import numbits_to_nums, numbits_union, nums_to_numbits
+from coverage.types import FilePath, TArc, TDebugCtl, TLineNo, TWarnFn
from coverage.version import __version__
os = isolate_module(os)
-# If you change the schema, increment the SCHEMA_VERSION, and update the
+# If you change the schema: increment the SCHEMA_VERSION and update the
# docs in docs/dbschema.rst by running "make cogdoc".
SCHEMA_VERSION = 7
@@ -52,7 +61,7 @@
key text,
value text,
unique (key)
- -- Keys:
+ -- Possible keys:
-- 'has_arcs' boolean -- Is this data recording branches?
-- 'sys_argv' text -- The coverage command line that recorded the data.
-- 'version' text -- The version of coverage.py that made the file.
@@ -103,7 +112,22 @@
);
"""
-class CoverageData(SimpleReprMixin):
+TMethod = TypeVar("TMethod", bound=Callable[..., Any])
+
+def _locked(method: TMethod) -> TMethod:
+ """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__}")
+ with self._lock:
+ if self._debug.should("lock"):
+ self._debug.write(f"Locked {self._lock!r} for {method.__name__}")
+ return method(self, *args, **kwargs)
+ return _wrapped # type: ignore[return-value]
+
+
+class CoverageData(AutoReprMixin):
"""Manages collected coverage data, including file storage.
This class is the public supported API to the data that coverage.py
@@ -173,9 +197,11 @@
Write the data to its file with :meth:`write`.
- You can clear the data in memory with :meth:`erase`. Two data collections
- can be combined by using :meth:`update` on one :class:`CoverageData`,
- passing it the other.
+ You can clear the data in memory with :meth:`erase`. Data for specific
+ files can be removed from the database with :meth:`purge_files`.
+
+ Two data collections can be combined by using :meth:`update` on one
+ :class:`CoverageData`, passing it the other.
Data in a :class:`CoverageData` can be serialized and deserialized with
:meth:`dumps` and :meth:`loads`.
@@ -186,7 +212,14 @@
"""
- def __init__(self, basename=None, suffix=None, no_disk=False, warn=None, debug=None):
+ def __init__(
+ self,
+ basename: Optional[FilePath] = None,
+ suffix: Optional[Union[str, bool]] = None,
+ no_disk: bool = False,
+ warn: Optional[TWarnFn] = None,
+ debug: Optional[TDebugCtl] = None,
+ ) -> None:
"""Create a :class:`CoverageData` object to hold coverage-measured data.
Arguments:
@@ -208,9 +241,10 @@
self._debug = debug or NoDebugging()
self._choose_filename()
- self._file_map = {}
+ # Maps filenames to row ids.
+ self._file_map: Dict[str, int] = {}
# Maps thread ids to SqliteDb objects.
- self._dbs = {}
+ self._dbs: Dict[int, SqliteDb] = {}
self._pid = os.getpid()
# Synchronize the operations used during collection.
self._lock = threading.RLock()
@@ -221,24 +255,11 @@
self._has_lines = False
self._has_arcs = False
- self._current_context = None
- self._current_context_id = None
- self._query_context_ids = None
-
- def _locked(method): # pylint: disable=no-self-argument
- """A decorator for methods that should hold self._lock."""
- @functools.wraps(method)
- def _wrapped(self, *args, **kwargs):
- 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__}")
- # pylint: disable=not-callable
- return method(self, *args, **kwargs)
- return _wrapped
+ self._current_context: Optional[str] = None
+ self._current_context_id: Optional[int] = None
+ self._query_context_ids: Optional[List[int]] = None
- def _choose_filename(self):
+ def _choose_filename(self) -> None:
"""Set self._filename based on inited attributes."""
if self._no_disk:
self._filename = ":memory:"
@@ -248,7 +269,7 @@
if suffix:
self._filename += "." + suffix
- def _reset(self):
+ def _reset(self) -> None:
"""Reset our attributes."""
if not self._no_disk:
for db in self._dbs.values():
@@ -258,18 +279,19 @@
self._have_used = False
self._current_context_id = None
- def _open_db(self):
+ 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}")
self._dbs[threading.get_ident()] = SqliteDb(self._filename, self._debug)
self._read_db()
- def _read_db(self):
+ def _read_db(self) -> None:
"""Read the metadata from a database so that we are ready to use it."""
with self._dbs[threading.get_ident()] as db:
try:
- schema_version, = 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):
self._init_db(db)
@@ -280,6 +302,7 @@
)
) from exc
else:
+ schema_version = row[0]
if schema_version != SCHEMA_VERSION:
raise DataError(
"Couldn't use data file {!r}: wrong schema: {} instead of {}".format(
@@ -287,46 +310,51 @@
)
)
- for row in db.execute("select value from meta where key = 'has_arcs'"):
+ row = db.execute_one("select value from meta where key = 'has_arcs'")
+ if row is not None:
self._has_arcs = bool(int(row[0]))
self._has_lines = not self._has_arcs
- for file_id, path in db.execute("select id, path from file"):
- self._file_map[path] = file_id
+ 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):
+ 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}")
db.executescript(SCHEMA)
- db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,))
- db.executemany(
- "insert or ignore into meta (key, value) values (?, ?)",
- [
+ 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__),
+ ]
+ if self._debug.should("process"):
+ meta_data.extend([
("sys_argv", str(getattr(sys, "argv", None))),
- ("version", __version__),
("when", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
- ]
- )
+ ])
+ db.executemany_void("insert or ignore into meta (key, value) values (?, ?)", meta_data)
- def _connect(self):
+ def _connect(self) -> SqliteDb:
"""Get the SqliteDb object to use."""
if threading.get_ident() not in self._dbs:
self._open_db()
return self._dbs[threading.get_ident()]
- def __bool__(self):
+ def __bool__(self) -> bool:
if (threading.get_ident() not in self._dbs and not os.path.exists(self._filename)):
return False
try:
with self._connect() as con:
- rows = con.execute("select * from file limit 1")
- return bool(list(rows))
+ with con.execute("select * from file limit 1") as cur:
+ return bool(list(cur))
except CoverageException:
return False
- @contract(returns="bytes")
- def dumps(self):
+ def dumps(self) -> bytes:
"""Serialize the current data to a byte string.
The format of the serialized data is not documented. It is only
@@ -349,8 +377,7 @@
script = con.dump()
return b"z" + zlib.compress(script.encode("utf-8"))
- @contract(data="bytes")
- def loads(self, data):
+ def loads(self, data: bytes) -> None:
"""Deserialize data from :meth:`dumps`.
Use with a newly-created empty :class:`CoverageData` object. It's
@@ -378,7 +405,7 @@
self._read_db()
self._have_used = True
- def _file_id(self, filename, add=False):
+ def _file_id(self, filename: str, add: bool = False) -> Optional[int]:
"""Get the file id for `filename`.
If filename is not in the database yet, add it if `add` is True.
@@ -393,19 +420,19 @@
)
return self._file_map.get(filename)
- def _context_id(self, context):
+ def _context_id(self, context: str) -> Optional[int]:
"""Get the id for a context."""
assert context is not None
self._start_using()
with self._connect() as con:
row = con.execute_one("select id from context where context = ?", (context,))
if row is not None:
- return row[0]
+ return cast(int, row[0])
else:
return None
@_locked
- def set_context(self, context):
+ def set_context(self, context: Optional[str]) -> None:
"""Set the current context for future :meth:`add_lines` etc.
`context` is a str, the name of the context to use for the next data
@@ -419,7 +446,7 @@
self._current_context = context
self._current_context_id = None
- def _set_context_id(self):
+ def _set_context_id(self) -> None:
"""Use the _current_context to set _current_context_id."""
context = self._current_context or ""
context_id = self._context_id(context)
@@ -432,7 +459,7 @@
(context,)
)
- def base_filename(self):
+ def base_filename(self) -> str:
"""The base filename for storing data.
.. versionadded:: 5.0
@@ -440,7 +467,7 @@
"""
return self._basename
- def data_filename(self):
+ def data_filename(self) -> str:
"""Where is the data stored?
.. versionadded:: 5.0
@@ -449,7 +476,7 @@
return self._filename
@_locked
- def add_lines(self, line_data):
+ def add_lines(self, line_data: Mapping[str, Collection[TLineNo]]) -> None:
"""Add measured line data.
`line_data` is a dictionary mapping file names to iterables of ints::
@@ -459,7 +486,7 @@
"""
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())
+ len(line_data), sum(bool(len(lines)) for lines in line_data.values())
))
self._start_using()
self._choose_lines_or_arcs(lines=True)
@@ -471,18 +498,19 @@
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 = ?"
- existing = list(con.execute(query, (file_id, self._current_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(
+ con.execute_void(
"insert or replace into line_bits " +
" (file_id, context_id, numbits) values (?, ?, ?)",
(file_id, self._current_context_id, linemap),
)
@_locked
- def add_arcs(self, arc_data):
+ def add_arcs(self, arc_data: Mapping[str, Collection[TArc]]) -> None:
"""Add measured arc data.
`arc_data` is a dictionary mapping file names to iterables of pairs of
@@ -502,15 +530,17 @@
with self._connect() as con:
self._set_context_id()
for filename, arcs in arc_data.items():
+ if not arcs:
+ continue
file_id = self._file_id(filename, add=True)
data = [(file_id, self._current_context_id, fromno, tono) for fromno, tono in arcs]
- con.executemany(
+ con.executemany_void(
"insert or ignore into arc " +
"(file_id, context_id, fromno, tono) values (?, ?, ?, ?)",
data,
)
- def _choose_lines_or_arcs(self, lines=False, arcs=False):
+ def _choose_lines_or_arcs(self, lines: bool = False, arcs: bool = False) -> None:
"""Force the data file to choose between lines and arcs."""
assert lines or arcs
assert not (lines and arcs)
@@ -526,13 +556,13 @@
self._has_lines = lines
self._has_arcs = arcs
with self._connect() as con:
- con.execute(
+ con.execute_void(
"insert or ignore into meta (key, value) values (?, ?)",
("has_arcs", str(int(arcs)))
)
@_locked
- def add_file_tracers(self, file_tracers):
+ def add_file_tracers(self, file_tracers: Mapping[str, str]) -> None:
"""Add per-file plugin information.
`file_tracers` is { filename: plugin_name, ... }
@@ -545,12 +575,7 @@
self._start_using()
with self._connect() as con:
for filename, plugin_name in file_tracers.items():
- file_id = self._file_id(filename)
- if file_id is None:
- raise DataError(
- f"Can't add file tracer data for unmeasured file '{filename}'"
- )
-
+ file_id = self._file_id(filename, add=True)
existing_plugin = self.file_tracer(filename)
if existing_plugin:
if existing_plugin != plugin_name:
@@ -560,24 +585,24 @@
)
)
elif plugin_name:
- con.execute(
+ con.execute_void(
"insert into tracer (file_id, tracer) values (?, ?)",
(file_id, plugin_name)
)
- def touch_file(self, filename, plugin_name=""):
+ 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. It is used
- to associate the right filereporter, etc.
+ `plugin_name` is the name of the plugin responsible for this file.
+ It is used to associate the right filereporter, etc.
"""
self.touch_files([filename], plugin_name)
- def touch_files(self, filenames, plugin_name=""):
+ def touch_files(self, filenames: Collection[str], plugin_name: Optional[str] = None) -> None:
"""Ensure that `filenames` appear in the data, empty if needed.
- `plugin_name` is the name of the plugin responsible for these files. It is used
- to associate the right filereporter, etc.
+ `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}")
@@ -592,11 +617,37 @@
# Set the tracer for this file
self.add_file_tracers({filename: plugin_name})
- def update(self, other_data, aliases=None):
+ def purge_files(self, filenames: Collection[str]) -> None:
+ """Purge any existing coverage data for the given `filenames`.
+
+ .. versionadded:: 7.2
+
+ """
+ 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=?"
+ elif self._has_arcs:
+ sql = "delete from arc where file_id=?"
+ else:
+ raise DataError("Can't purge files in an empty CoverageData")
+
+ for filename in filenames:
+ file_id = self._file_id(filename, add=False)
+ if file_id is None:
+ continue
+ con.execute_void(sql, (file_id,))
+
+ def update(self, other_data: CoverageData, aliases: Optional[PathAliases] = None) -> None:
"""Update this data with data from several other :class:`CoverageData` instances.
If `aliases` is provided, it's a `PathAliases` object that is used to
- re-map paths to match the local machine's.
+ re-map paths to match the local machine's. Note: `aliases` is None
+ only when called directly from the test suite.
+
"""
if self._debug.should("dataop"):
self._debug.write("Updating with data from {!r}".format(
@@ -616,78 +667,80 @@
other_data.read()
with other_data._connect() as con:
# Get files data.
- cur = con.execute("select path from file")
- files = {path: aliases.map(path) for (path,) in cur}
- cur.close()
+ with con.execute("select path from file") as cur:
+ files = {path: aliases.map(path) for (path,) in cur}
# Get contexts data.
- cur = con.execute("select context from context")
- contexts = [context for (context,) in cur]
- cur.close()
+ with con.execute("select context from context") as cur:
+ contexts = [context for (context,) in cur]
# Get arc data.
- cur = con.execute(
+ 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"
- )
- arcs = [(files[path], context, fromno, tono) for (path, context, fromno, tono) in cur]
- cur.close()
+ ) as cur:
+ arcs = [
+ (files[path], context, fromno, tono)
+ for (path, context, fromno, tono) in cur
+ ]
# Get line data.
- cur = con.execute(
+ 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"
- )
- lines = {(files[path], context): numbits for (path, context, numbits) in cur}
- cur.close()
+ ) as cur:
+ lines: Dict[Tuple[str, str], bytes] = {}
+ for path, context, numbits in cur:
+ key = (files[path], context)
+ if key in lines:
+ numbits = numbits_union(lines[key], numbits)
+ lines[key] = numbits
# Get tracer data.
- cur = con.execute(
+ with con.execute(
"select file.path, tracer " +
"from tracer " +
"inner join file on file.id = tracer.file_id"
- )
- tracers = {files[path]: tracer for (path, tracer) in cur}
- cur.close()
+ ) 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"
# 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.
- this_tracers = {path: "" for path, in con.execute("select path from file")}
- this_tracers.update({
- aliases.map(path): tracer
- for path, tracer in con.execute(
- "select file.path, tracer from tracer " +
- "inner join file on file.id = tracer.file_id"
- )
- })
+ 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"
+ ) as cur:
+ this_tracers.update({
+ aliases.map(path): tracer
+ for path, tracer in cur
+ })
# Create all file and context rows in the DB.
- con.executemany(
+ con.executemany_void(
"insert or ignore into file (path) values (?)",
((file,) for file in files.values())
)
- file_ids = {
- path: id
- for id, path in con.execute("select id, path from file")
- }
+ 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(
+ con.executemany_void(
"insert or ignore into context (context) values (?)",
((context,) for context in contexts)
)
- context_ids = {
- context: id
- for id, context in con.execute("select id, context from context")
- }
+ 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.
# tracer_paths is used to ensure consistency over the tracer data
@@ -714,24 +767,23 @@
)
# Get line data.
- cur = con.execute(
+ 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"
- )
- for path, context, numbits in cur:
- key = (aliases.map(path), context)
- if key in lines:
- numbits = numbits_union(lines[key], numbits)
- lines[key] = numbits
- cur.close()
+ ) as cur:
+ for path, context, numbits in cur:
+ key = (aliases.map(path), context)
+ if key in lines:
+ numbits = numbits_union(lines[key], numbits)
+ lines[key] = numbits
if arcs:
self._choose_lines_or_arcs(arcs=True)
# Write the combined data.
- con.executemany(
+ con.executemany_void(
"insert or ignore into arc " +
"(file_id, context_id, fromno, tono) values (?, ?, ?, ?)",
arc_rows
@@ -739,8 +791,8 @@
if lines:
self._choose_lines_or_arcs(lines=True)
- con.execute("delete from line_bits")
- con.executemany(
+ con.execute_void("delete from line_bits")
+ con.executemany_void(
"insert into line_bits " +
"(file_id, context_id, numbits) values (?, ?, ?)",
[
@@ -748,7 +800,7 @@
for (file, context), numbits in lines.items()
]
)
- con.executemany(
+ con.executemany_void(
"insert or ignore into tracer (file_id, tracer) values (?, ?)",
((file_ids[filename], tracer) for filename, tracer in tracer_map.items())
)
@@ -758,7 +810,7 @@
self._reset()
self.read()
- def erase(self, parallel=False):
+ def erase(self, parallel: bool = False) -> None:
"""Erase the data in this object.
If `parallel` is true, then also deletes data files created from the
@@ -780,17 +832,17 @@
self._debug.write(f"Erasing parallel data file {filename!r}")
file_be_gone(filename)
- def read(self):
+ def read(self) -> None:
"""Start using an existing data file."""
if os.path.exists(self._filename):
with self._connect():
self._have_used = True
- def write(self):
+ def write(self) -> None:
"""Ensure the data is written to the data file."""
pass
- def _start_using(self):
+ def _start_using(self) -> None:
"""Call this before using the database at all."""
if self._pid != os.getpid():
# Looks like we forked! Have to start a new data file.
@@ -801,15 +853,20 @@
self.erase()
self._have_used = True
- def has_arcs(self):
+ def has_arcs(self) -> bool:
"""Does the database have arcs (True) or lines (False)."""
return bool(self._has_arcs)
- def measured_files(self):
- """A set of all files that had been measured."""
+ def measured_files(self) -> Set[str]:
+ """A set of all files that have been measured.
+
+ Note that a file may be mentioned as measured even though no lines or
+ arcs for that file are present in the data.
+
+ """
return set(self._file_map)
- def measured_contexts(self):
+ def measured_contexts(self) -> Set[str]:
"""A set of all contexts that have been measured.
.. versionadded:: 5.0
@@ -817,10 +874,11 @@
"""
self._start_using()
with self._connect() as con:
- contexts = {row[0] for row in con.execute("select distinct(context) from context")}
+ with con.execute("select distinct(context) from context") as cur:
+ contexts = {row[0] for row in cur}
return contexts
- def file_tracer(self, filename):
+ def file_tracer(self, filename: str) -> Optional[str]:
"""Get the plugin name of the file tracer for a file.
Returns the name of the plugin that handles this file. If the file was
@@ -838,7 +896,7 @@
return row[0] or ""
return "" # File was measured, but no tracer associated.
- def set_query_context(self, context):
+ def set_query_context(self, context: str) -> None:
"""Set a context for subsequent querying.
The next :meth:`lines`, :meth:`arcs`, or :meth:`contexts_by_lineno`
@@ -851,10 +909,10 @@
"""
self._start_using()
with self._connect() as con:
- cur = con.execute("select id from context where context = ?", (context,))
- self._query_context_ids = [row[0] for row in cur.fetchall()]
+ 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):
+ def set_query_contexts(self, contexts: Optional[Sequence[str]]) -> None:
"""Set a number of contexts for subsequent querying.
The next :meth:`lines`, :meth:`arcs`, or :meth:`contexts_by_lineno`
@@ -870,12 +928,12 @@
if contexts:
with self._connect() as con:
context_clause = " or ".join(["context regexp ?"] * len(contexts))
- cur = con.execute("select id from context where " + context_clause, contexts)
- self._query_context_ids = [row[0] for row in cur.fetchall()]
+ 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
- def lines(self, filename):
+ def lines(self, filename: str) -> Optional[List[TLineNo]]:
"""Get the list of lines executed for a source file.
If the file was not measured, returns None. A file might be measured,
@@ -903,13 +961,14 @@
ids_array = ", ".join("?" * len(self._query_context_ids))
query += " and context_id in (" + ids_array + ")"
data += self._query_context_ids
- bitmaps = list(con.execute(query, data))
+ with con.execute(query, data) as cur:
+ bitmaps = list(cur)
nums = set()
for row in bitmaps:
nums.update(numbits_to_nums(row[0]))
return list(nums)
- def arcs(self, filename):
+ def arcs(self, filename: str) -> Optional[List[TArc]]:
"""Get the list of arcs executed for a file.
If the file was not measured, returns None. A file might be measured,
@@ -938,10 +997,10 @@
ids_array = ", ".join("?" * len(self._query_context_ids))
query += " and context_id in (" + ids_array + ")"
data += self._query_context_ids
- arcs = con.execute(query, data)
- return list(arcs)
+ with con.execute(query, data) as cur:
+ return list(cur)
- def contexts_by_lineno(self, filename):
+ def contexts_by_lineno(self, filename: str) -> Dict[TLineNo, List[str]]:
"""Get the contexts for each line in a file.
Returns:
@@ -968,11 +1027,12 @@
ids_array = ", ".join("?" * len(self._query_context_ids))
query += " and arc.context_id in (" + ids_array + ")"
data += self._query_context_ids
- for fromno, tono, context in con.execute(query, data):
- if fromno > 0:
- lineno_contexts_map[fromno].add(context)
- if tono > 0:
- lineno_contexts_map[tono].add(context)
+ with con.execute(query, data) as cur:
+ for fromno, tono, context in cur:
+ if fromno > 0:
+ lineno_contexts_map[fromno].add(context)
+ if tono > 0:
+ lineno_contexts_map[tono].add(context)
else:
query = (
"select l.numbits, c.context from line_bits l, context c " +
@@ -984,33 +1044,35 @@
ids_array = ", ".join("?" * len(self._query_context_ids))
query += " and l.context_id in (" + ids_array + ")"
data += self._query_context_ids
- for numbits, context in con.execute(query, data):
- for lineno in numbits_to_nums(numbits):
- lineno_contexts_map[lineno].add(context)
+ with con.execute(query, data) as cur:
+ for numbits, context in cur:
+ for lineno in numbits_to_nums(numbits):
+ lineno_contexts_map[lineno].add(context)
return {lineno: list(contexts) for lineno, contexts in lineno_contexts_map.items()}
@classmethod
- def sys_info(cls):
+ def sys_info(cls) -> List[Tuple[str, Any]]:
"""Our information for `Coverage.sys_info`.
Returns a list of (key, value) pairs.
"""
with SqliteDb(":memory:", debug=NoDebugging()) as db:
- temp_store = [row[0] for row in db.execute("pragma temp_store")]
- copts = [row[0] for row in db.execute("pragma compile_options")]
+ with db.execute("pragma temp_store") as cur:
+ temp_store = [row[0] for row in cur]
+ with db.execute("pragma compile_options") as cur:
+ copts = [row[0] for row in cur]
copts = textwrap.wrap(", ".join(copts), width=75)
return [
- ("sqlite3_version", sqlite3.version),
("sqlite3_sqlite_version", sqlite3.sqlite_version),
("sqlite3_temp_store", temp_store),
("sqlite3_compile_options", copts),
]
-def filename_suffix(suffix):
+def filename_suffix(suffix: Union[str, bool, None]) -> Union[str, None]:
"""Compute a filename suffix for a data file.
If `suffix` is a string or None, simply return it. If `suffix` is True,
@@ -1027,10 +1089,12 @@
# if the process forks.
dice = random.Random(os.urandom(8)).randint(0, 999999)
suffix = "%s.%s.%06d" % (socket.gethostname(), os.getpid(), dice)
+ elif suffix is False:
+ suffix = None
return suffix
-class SqliteDb(SimpleReprMixin):
+class SqliteDb(AutoReprMixin):
"""A simple abstraction over a SQLite database.
Use as a context manager, then you can use it like a
@@ -1040,13 +1104,13 @@
db.execute("insert into schema (version) values (?)", (SCHEMA_VERSION,))
"""
- def __init__(self, filename, debug):
+ def __init__(self, filename: str, debug: TDebugCtl) -> None:
self.debug = debug
self.filename = filename
self.nest = 0
- self.con = None
+ self.con: Optional[sqlite3.Connection] = None
- def _connect(self):
+ def _connect(self) -> None:
"""Connect to the db and do universal initialization."""
if self.con is not None:
return
@@ -1068,27 +1132,29 @@
# This pragma makes writing faster. It disables rollbacks, but we never need them.
# PyPy needs the .close() calls here, or sqlite gets twisted up:
# https://bitbucket.org/pypy/pypy/issues/2872/default-isolation-mode-is-different-on
- self.execute("pragma journal_mode=off").close()
+ self.execute_void("pragma journal_mode=off")
# This pragma makes writing faster.
- self.execute("pragma synchronous=off").close()
+ self.execute_void("pragma synchronous=off")
- def close(self):
+ def close(self) -> None:
"""If needed, close the connection."""
if self.con is not None and self.filename != ":memory:":
self.con.close()
self.con = None
- def __enter__(self):
+ def __enter__(self) -> SqliteDb:
if self.nest == 0:
self._connect()
+ assert self.con is not None
self.con.__enter__()
self.nest += 1
return self
- def __exit__(self, exc_type, exc_value, traceback):
+ def __exit__(self, exc_type, exc_value, traceback) -> None: # type: ignore[no-untyped-def]
self.nest -= 1
if self.nest == 0:
try:
+ assert self.con is not None
self.con.__exit__(exc_type, exc_value, traceback)
self.close()
except Exception as exc:
@@ -1096,19 +1162,20 @@
self.debug.write(f"EXCEPTION from __exit__: {exc}")
raise DataError(f"Couldn't end data file {self.filename!r}: {exc}") from exc
- def execute(self, sql, parameters=()):
+ 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}")
try:
+ assert self.con is not None
try:
- return self.con.execute(sql, parameters)
+ return self.con.execute(sql, parameters) # type: ignore[arg-type]
except Exception:
# In some cases, an error might happen that isn't really an
# error. Try again immediately.
# https://github.com/nedbat/coveragepy/issues/1010
- return self.con.execute(sql, parameters)
+ return self.con.execute(sql, parameters) # type: ignore[arg-type]
except sqlite3.Error as exc:
msg = str(exc)
try:
@@ -1127,15 +1194,36 @@
self.debug.write(f"EXCEPTION from execute: {msg}")
raise DataError(f"Couldn't use data file {self.filename!r}: {msg}") from exc
- def execute_for_rowid(self, sql, parameters=()):
+ @contextlib.contextmanager
+ def execute(
+ self,
+ sql: str,
+ parameters: Iterable[Any] = (),
+ ) -> Iterator[sqlite3.Cursor]:
+ """Context managed :meth:`python:sqlite3.Connection.execute`.
+
+ Use with a ``with`` statement to auto-close the returned cursor.
+ """
+ cur = self._execute(sql, parameters)
+ try:
+ yield cur
+ finally:
+ cur.close()
+
+ def execute_void(self, sql: str, parameters: Iterable[Any] = ()) -> None:
+ """Same as :meth:`python:sqlite3.Connection.execute` when you don't need the cursor."""
+ self._execute(sql, parameters).close()
+
+ def execute_for_rowid(self, sql: str, parameters: Iterable[Any] = ()) -> int:
"""Like execute, but returns the lastrowid."""
- con = self.execute(sql, parameters)
- rowid = con.lastrowid
+ 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}")
return rowid
- def execute_one(self, sql, parameters=()):
+ def execute_one(self, sql: str, parameters: Iterable[Any] = ()) -> Optional[Tuple[Any, ...]]:
"""Execute a statement and return the one row that results.
This is like execute(sql, parameters).fetchone(), except it is
@@ -1144,23 +1232,24 @@
Returns a row, or None if there were no rows.
"""
- rows = list(self.execute(sql, parameters))
+ with self.execute(sql, parameters) as cur:
+ rows = list(cur)
if len(rows) == 0:
return None
elif len(rows) == 1:
- return rows[0]
+ return cast(Tuple[Any, ...], rows[0])
else:
raise AssertionError(f"SQL {sql!r} shouldn't return {len(rows)} rows")
- def executemany(self, sql, data):
+ def _executemany(self, sql: str, data: List[Any]) -> sqlite3.Cursor:
"""Same as :meth:`python:sqlite3.Connection.executemany`."""
if self.debug.should("sql"):
- data = list(data)
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}")
+ assert self.con is not None
try:
return self.con.executemany(sql, data)
except Exception: # pragma: cant happen
@@ -1169,14 +1258,22 @@
# https://github.com/nedbat/coveragepy/issues/1010
return self.con.executemany(sql, data)
- def executescript(self, script):
+ def executemany_void(self, sql: str, data: Iterable[Any]) -> None:
+ """Same as :meth:`python:sqlite3.Connection.executemany` when you don't need the cursor."""
+ data = list(data)
+ if data:
+ self._executemany(sql, data).close()
+
+ 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),
))
- self.con.executescript(script)
+ assert self.con is not None
+ self.con.executescript(script).close()
- def dump(self):
+ 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())
diff -Nru python-coverage-6.5.0+dfsg1/coverage/summary.py python-coverage-7.2.7+dfsg1/coverage/summary.py
--- python-coverage-6.5.0+dfsg1/coverage/summary.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/summary.py 1970-01-01 00:00:00.000000000 +0000
@@ -1,152 +0,0 @@
-# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
-# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
-
-"""Summary reporting"""
-
-import sys
-
-from coverage.exceptions import ConfigError, NoDataError
-from coverage.misc import human_sorted_items
-from coverage.report import get_analysis_to_report
-from coverage.results import Numbers
-
-
-class SummaryReporter:
- """A reporter for writing the summary report."""
-
- def __init__(self, coverage):
- self.coverage = coverage
- self.config = self.coverage.config
- self.branches = coverage.get_data().has_arcs()
- self.outfile = None
- self.fr_analysis = []
- self.skipped_count = 0
- self.empty_count = 0
- self.total = Numbers(precision=self.config.precision)
- self.fmt_err = "%s %s: %s"
-
- def writeout(self, line):
- """Write a line to the output, adding a newline."""
- self.outfile.write(line.rstrip())
- self.outfile.write("\n")
-
- def report(self, morfs, outfile=None):
- """Writes a report summarizing coverage statistics per module.
-
- `outfile` is a file object to write the summary to. It must be opened
- for native strings (bytes on Python 2, Unicode on Python 3).
-
- """
- self.outfile = outfile or sys.stdout
-
- self.coverage.get_data().set_query_contexts(self.config.report_contexts)
- for fr, analysis in get_analysis_to_report(self.coverage, morfs):
- self.report_one_file(fr, analysis)
-
- # Prepare the formatting strings, header, and column sorting.
- max_name = max([len(fr.relative_filename()) for (fr, analysis) in self.fr_analysis] + [5])
- fmt_name = "%%- %ds " % max_name
- fmt_skip_covered = "\n%s file%s skipped due to complete coverage."
- fmt_skip_empty = "\n%s empty file%s skipped."
-
- header = (fmt_name % "Name") + " Stmts Miss"
- fmt_coverage = fmt_name + "%6d %6d"
- if self.branches:
- header += " Branch BrPart"
- fmt_coverage += " %6d %6d"
- width100 = Numbers(precision=self.config.precision).pc_str_width()
- header += "%*s" % (width100+4, "Cover")
- fmt_coverage += "%%%ds%%%%" % (width100+3,)
- if self.config.show_missing:
- header += " Missing"
- fmt_coverage += " %s"
- rule = "-" * len(header)
-
- column_order = dict(name=0, stmts=1, miss=2, cover=-1)
- if self.branches:
- column_order.update(dict(branch=3, brpart=4))
-
- # Write the header
- self.writeout(header)
- self.writeout(rule)
-
- # `lines` is a list of pairs, (line text, line values). The line text
- # is a string that will be printed, and line values is a tuple of
- # sortable values.
- lines = []
-
- for (fr, analysis) in self.fr_analysis:
- nums = analysis.numbers
-
- args = (fr.relative_filename(), nums.n_statements, nums.n_missing)
- if self.branches:
- args += (nums.n_branches, nums.n_partial_branches)
- args += (nums.pc_covered_str,)
- if self.config.show_missing:
- args += (analysis.missing_formatted(branches=True),)
- text = fmt_coverage % args
- # Add numeric percent coverage so that sorting makes sense.
- args += (nums.pc_covered,)
- lines.append((text, args))
-
- # Sort the lines and write them out.
- sort_option = (self.config.sort or "name").lower()
- reverse = False
- if sort_option[0] == '-':
- reverse = True
- sort_option = sort_option[1:]
- elif sort_option[0] == '+':
- sort_option = sort_option[1:]
-
- if sort_option == "name":
- lines = human_sorted_items(lines, reverse=reverse)
- else:
- position = column_order.get(sort_option)
- if position is None:
- raise ConfigError(f"Invalid sorting option: {self.config.sort!r}")
- lines.sort(key=lambda l: (l[1][position], l[0]), reverse=reverse)
-
- for line in lines:
- self.writeout(line[0])
-
- # Write a TOTAL line if we had at least one file.
- if self.total.n_files > 0:
- self.writeout(rule)
- args = ("TOTAL", self.total.n_statements, self.total.n_missing)
- if self.branches:
- args += (self.total.n_branches, self.total.n_partial_branches)
- args += (self.total.pc_covered_str,)
- if self.config.show_missing:
- args += ("",)
- self.writeout(fmt_coverage % args)
-
- # Write other final lines.
- if not self.total.n_files and not self.skipped_count:
- raise NoDataError("No data to report.")
-
- if self.config.skip_covered and self.skipped_count:
- self.writeout(
- fmt_skip_covered % (self.skipped_count, 's' if self.skipped_count > 1 else '')
- )
- if self.config.skip_empty and self.empty_count:
- self.writeout(
- fmt_skip_empty % (self.empty_count, 's' if self.empty_count > 1 else '')
- )
-
- return self.total.n_statements and self.total.pc_covered
-
- def report_one_file(self, fr, analysis):
- """Report on just one file, the callback from report()."""
- nums = analysis.numbers
- self.total += nums
-
- no_missing_lines = (nums.n_missing == 0)
- no_missing_branches = (nums.n_partial_branches == 0)
- if self.config.skip_covered and no_missing_lines and no_missing_branches:
- # Don't report on 100% files.
- self.skipped_count += 1
- elif self.config.skip_empty and nums.n_statements == 0:
- # Don't report on empty files.
- self.empty_count += 1
- else:
- self.fr_analysis.append((fr, analysis))
diff -Nru python-coverage-6.5.0+dfsg1/coverage/templite.py python-coverage-7.2.7+dfsg1/coverage/templite.py
--- python-coverage-6.5.0+dfsg1/coverage/templite.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/templite.py 2023-05-29 19:46:30.000000000 +0000
@@ -10,8 +10,14 @@
# Coincidentally named the same as http://code.activestate.com/recipes/496702/
+from __future__ import annotations
+
import re
+from typing import (
+ Any, Callable, Dict, List, NoReturn, Optional, Set, Union, cast,
+)
+
class TempliteSyntaxError(ValueError):
"""Raised when a template has a syntax error."""
@@ -26,14 +32,14 @@
class CodeBuilder:
"""Build source code conveniently."""
- def __init__(self, indent=0):
- self.code = []
+ def __init__(self, indent: int = 0) -> None:
+ self.code: List[Union[str, CodeBuilder]] = []
self.indent_level = indent
- def __str__(self):
+ def __str__(self) -> str:
return "".join(str(c) for c in self.code)
- def add_line(self, line):
+ def add_line(self, line: str) -> None:
"""Add a line of source to the code.
Indentation and newline will be added for you, don't provide them.
@@ -41,7 +47,7 @@
"""
self.code.extend([" " * self.indent_level, line, "\n"])
- def add_section(self):
+ def add_section(self) -> CodeBuilder:
"""Add a section, a sub-CodeBuilder."""
section = CodeBuilder(self.indent_level)
self.code.append(section)
@@ -49,22 +55,22 @@
INDENT_STEP = 4 # PEP8 says so!
- def indent(self):
+ def indent(self) -> None:
"""Increase the current indent for following lines."""
self.indent_level += self.INDENT_STEP
- def dedent(self):
+ def dedent(self) -> None:
"""Decrease the current indent for following lines."""
self.indent_level -= self.INDENT_STEP
- def get_globals(self):
+ def get_globals(self) -> Dict[str, Any]:
"""Execute the code, and return a dict of globals it defines."""
# A check that the caller really finished all the blocks they started.
assert self.indent_level == 0
# Get the Python source as a single string.
python_source = str(self)
# Execute the source, defining globals, and return them.
- global_namespace = {}
+ global_namespace: Dict[str, Any] = {}
exec(python_source, global_namespace)
return global_namespace
@@ -92,7 +98,7 @@
and joined. Be careful, this could join words together!
Any of these constructs can have a hyphen at the end (`-}}`, `-%}`, `-#}`),
- which will collapse the whitespace following the tag.
+ which will collapse the white space following the tag.
Construct a Templite with the template text, then use `render` against a
dictionary context to create a finished string::
@@ -103,15 +109,15 @@
You are interested in {{topic}}.
{% endif %}
''',
- {'upper': str.upper},
+ {"upper": str.upper},
)
text = templite.render({
- 'name': "Ned",
- 'topics': ['Python', 'Geometry', 'Juggling'],
+ "name": "Ned",
+ "topics": ["Python", "Geometry", "Juggling"],
})
"""
- def __init__(self, text, *contexts):
+ def __init__(self, text: str, *contexts: Dict[str, Any]) -> None:
"""Construct a Templite with the given `text`.
`contexts` are dictionaries of values to use for future renderings.
@@ -122,8 +128,8 @@
for context in contexts:
self.context.update(context)
- self.all_vars = set()
- self.loop_vars = set()
+ self.all_vars: Set[str] = set()
+ self.loop_vars: Set[str] = set()
# We construct a function in source form, then compile it and hold onto
# it, and execute it to render the template.
@@ -137,9 +143,9 @@
code.add_line("extend_result = result.extend")
code.add_line("to_str = str")
- buffered = []
+ buffered: List[str] = []
- def flush_output():
+ def flush_output() -> None:
"""Force `buffered` to the code builder."""
if len(buffered) == 1:
code.add_line("append_result(%s)" % buffered[0])
@@ -155,37 +161,37 @@
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)
else:
- # token.startswith('{%')
+ # 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')
+ 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(
"for c_{} in {}:".format(
@@ -194,10 +200,10 @@
)
)
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)
@@ -207,7 +213,7 @@
start_what = ops_stack.pop()
if start_what != end_what:
self._syntax_error("Mismatched end tag", end_what)
- if end_what == 'joined':
+ if end_what == "joined":
in_joined = False
else:
code.dedent()
@@ -230,11 +236,17 @@
for var_name in self.all_vars - self.loop_vars:
vars_code.add_line(f"c_{var_name} = context[{var_name!r}]")
- code.add_line('return "".join(result)')
+ code.add_line("return ''.join(result)")
code.dedent()
- self._render_function = code.get_globals()['render_function']
+ self._render_function = cast(
+ Callable[
+ [Dict[str, Any], Callable[..., Any]],
+ str
+ ],
+ code.get_globals()["render_function"],
+ )
- def _expr_code(self, expr):
+ def _expr_code(self, expr: str) -> str:
"""Generate a Python expression for `expr`."""
if "|" in expr:
pipes = expr.split("|")
@@ -252,11 +264,11 @@
code = "c_%s" % expr
return code
- def _syntax_error(self, msg, thing):
+ def _syntax_error(self, msg: str, thing: Any) -> NoReturn:
"""Raise a syntax error using `msg`, and showing `thing`."""
raise TempliteSyntaxError(f"{msg}: {thing!r}")
- def _variable(self, name, vars_set):
+ def _variable(self, name: str, vars_set: Set[str]) -> None:
"""Track that `name` is used as a variable.
Adds the name to `vars_set`, a set of variable names.
@@ -268,7 +280,7 @@
self._syntax_error("Not a valid name", name)
vars_set.add(name)
- def render(self, context=None):
+ def render(self, context: Optional[Dict[str, Any]] = None) -> str:
"""Render this template by applying it to `context`.
`context` is a dictionary of values to use in this rendering.
@@ -280,7 +292,7 @@
render_context.update(context)
return self._render_function(render_context, self._do_dots)
- def _do_dots(self, value, *dots):
+ def _do_dots(self, value: Any, *dots: str) -> Any:
"""Evaluate dotted expressions at run-time."""
for dot in dots:
try:
diff -Nru python-coverage-6.5.0+dfsg1/coverage/tomlconfig.py python-coverage-7.2.7+dfsg1/coverage/tomlconfig.py
--- python-coverage-6.5.0+dfsg1/coverage/tomlconfig.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/tomlconfig.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,25 +3,25 @@
"""TOML configuration support for coverage.py"""
-import configparser
+from __future__ import annotations
+
import os
import re
+from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, 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
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.
- # (Import typing is here because import_third_party will unload any module
- # that wasn't already imported. tomli imports typing, and if we unload it,
- # later it's imported again, and on Python 3.6, this causes infinite
- # recursion.)
- import typing # pylint: disable=unused-import
- tomllib = import_third_party("tomli")
+ tomllib, has_tomllib = import_third_party("tomli")
class TomlDecodeError(Exception):
@@ -29,6 +29,8 @@
pass
+TWant = TypeVar("TWant")
+
class TomlConfigParser:
"""TOML file reading with the interface of HandyConfigParser."""
@@ -36,11 +38,11 @@
# need for docstrings.
# pylint: disable=missing-function-docstring
- def __init__(self, our_file):
+ def __init__(self, our_file: bool) -> None:
self.our_file = our_file
- self.data = None
+ self.data: Dict[str, Any] = {}
- def read(self, filenames):
+ def read(self, filenames: Iterable[str]) -> List[str]:
# RawConfigParser takes a filename or list of filenames, but we only
# ever call this with a single filename.
assert isinstance(filenames, (bytes, str, os.PathLike))
@@ -51,22 +53,21 @@
toml_text = fp.read()
except OSError:
return []
- if tomllib is not None:
- toml_text = substitute_variables(toml_text, os.environ)
+ if has_tomllib:
try:
self.data = tomllib.loads(toml_text)
except tomllib.TOMLDecodeError as err:
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"
raise ConfigError(msg.format(filename))
return []
- def _get_section(self, section):
+ def _get_section(self, section: str) -> Tuple[Optional[str], Optional[TConfigSectionOut]]:
"""Get a section from the data.
Arguments:
@@ -79,8 +80,6 @@
"""
prefixes = ["tool.coverage."]
- if self.our_file:
- prefixes.append("")
for prefix in prefixes:
real_section = prefix + section
parts = real_section.split(".")
@@ -95,60 +94,101 @@
return None, None
return real_section, data
- def _get(self, section, option):
+ def _get(self, section: str, option: str) -> Tuple[str, TConfigValueOut]:
"""Like .get, but returns the real section name and the value."""
name, data = self._get_section(section)
if data is None:
- raise configparser.NoSectionError(section)
+ raise ConfigError(f"No section: {section!r}")
+ assert name is not None
try:
- return name, data[option]
- except KeyError as exc:
- raise configparser.NoOptionError(option, name) from exc
+ value = data[option]
+ except KeyError:
+ 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:
+ """Get a single-valued option.
+
+ Performs environment substitution if the value is a string. Other types
+ will be converted later as needed.
+ """
+ name, value = self._get(section, option)
+ if isinstance(value, str):
+ value = substitute_variables(value, os.environ)
+ return name, value
- def has_option(self, section, option):
+ def has_option(self, section: str, option: str) -> bool:
_, data = self._get_section(section)
if data is None:
return False
return option in data
- def has_section(self, section):
+ def real_section(self, section: str) -> Optional[str]:
name, _ = self._get_section(section)
return name
- def options(self, section):
+ def has_section(self, section: str) -> bool:
+ name, _ = self._get_section(section)
+ return bool(name)
+
+ def options(self, section: str) -> List[str]:
_, data = self._get_section(section)
if data is None:
- raise configparser.NoSectionError(section)
+ raise ConfigError(f"No section: {section!r}")
return list(data.keys())
- def get_section(self, section):
+ def get_section(self, section: str) -> TConfigSectionOut:
_, data = self._get_section(section)
- return data
+ return data or {}
- def get(self, section, option):
- _, value = self._get(section, option)
+ def get(self, section: str, option: str) -> Any:
+ _, value = self._get_single(section, option)
return value
- def _check_type(self, section, option, value, type_, type_desc):
- if not isinstance(value, type_):
- raise ValueError(
- 'Option {!r} in section {!r} is not {}: {!r}'
- .format(option, section, type_desc, value)
- )
+ def _check_type(
+ self,
+ section: str,
+ option: str,
+ value: Any,
+ type_: Type[TWant],
+ converter: Optional[Callable[[Any], TWant]],
+ type_desc: str,
+ ) -> TWant:
+ """Check that `value` has the type we want, converting if needed.
- def getboolean(self, section, option):
- name, value = self._get(section, option)
- self._check_type(name, option, value, bool, "a boolean")
- return value
+ Returns the resulting value of the desired type.
+ """
+ if isinstance(value, type_):
+ return value
+ if isinstance(value, str) and converter is not None:
+ try:
+ return converter(value)
+ except Exception as e:
+ raise ValueError(
+ 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}"
+ )
+
+ 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")
- def getlist(self, section, option):
+ 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)
- self._check_type(name, option, values, list, "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
+
+ def getlist(self, section: str, option: str) -> List[str]:
+ _, values = self._get_list(section, option)
return values
- def getregexlist(self, section, option):
- name, values = self._get(section, option)
- self._check_type(name, option, values, list, "a list")
+ def getregexlist(self, section: str, option: str) -> List[str]:
+ name, values = self._get_list(section, option)
for value in values:
value = value.strip()
try:
@@ -157,14 +197,12 @@
raise ConfigError(f"Invalid [{name}].{option} value {value!r}: {e}") from e
return values
- def getint(self, section, option):
- name, value = self._get(section, option)
- self._check_type(name, option, value, int, "an integer")
- return value
+ 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")
- def getfloat(self, section, option):
- name, value = self._get(section, option)
+ def getfloat(self, section: str, option: str) -> float:
+ name, value = self._get_single(section, option)
if isinstance(value, int):
value = float(value)
- self._check_type(name, option, value, float, "a float")
- return value
+ return self._check_type(name, option, value, float, float, "a float")
diff -Nru python-coverage-6.5.0+dfsg1/coverage/tracer.pyi python-coverage-7.2.7+dfsg1/coverage/tracer.pyi
--- python-coverage-6.5.0+dfsg1/coverage/tracer.pyi 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/tracer.pyi 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +1,35 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+from typing import Any, Dict
+
+from coverage.types import TFileDisposition, TTraceData, TTraceFn, TTracer
+
+class CFileDisposition(TFileDisposition):
+ canonical_filename: Any
+ file_tracer: Any
+ has_dynamic_filename: Any
+ original_filename: Any
+ reason: Any
+ source_filename: Any
+ trace: Any
+ def __init__(self) -> None: ...
+
+class CTracer(TTracer):
+ check_include: Any
+ concur_id_func: Any
+ data: TTraceData
+ disable_plugin: Any
+ file_tracers: Any
+ should_start_context: Any
+ should_trace: Any
+ should_trace_cache: Any
+ switch_context: Any
+ trace_arcs: Any
+ warn: Any
+ def __init__(self) -> None: ...
+ def activity(self) -> bool: ...
+ def get_stats(self) -> Dict[str, int]: ...
+ def reset_activity(self) -> Any: ...
+ def start(self) -> TTraceFn: ...
+ def stop(self) -> None: ...
diff -Nru python-coverage-6.5.0+dfsg1/coverage/types.py python-coverage-7.2.7+dfsg1/coverage/types.py
--- python-coverage-6.5.0+dfsg1/coverage/types.py 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/types.py 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +1,197 @@
+# 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, Set, Tuple, Type, Union,
+ TYPE_CHECKING,
+)
+
+if TYPE_CHECKING:
+ # Protocol is new in 3.8. PYVERSIONS
+ from typing import Protocol
+
+ from coverage.plugin import FileTracer
+
+else:
+ class Protocol: # pylint: disable=missing-class-docstring
+ pass
+
+## File paths
+
+# For arguments that are file paths:
+if TYPE_CHECKING:
+ FilePath = Union[str, os.PathLike[str]]
+else:
+ # PathLike < python3.9 doesn't support subscription
+ FilePath = Union[str, os.PathLike]
+# For testing FilePath arguments
+FilePathClasses = [str, pathlib.Path]
+FilePathType = Union[Type[str], Type[pathlib.Path]]
+
+## Python tracing
+
+class TTraceFn(Protocol):
+ """A Python trace function."""
+ def __call__(
+ self,
+ frame: FrameType,
+ event: str,
+ arg: Any,
+ lineno: Optional[TLineNo] = None # Our own twist, see collector.py
+ ) -> Optional[TTraceFn]:
+ ...
+
+## 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."""
+
+ original_filename: str
+ canonical_filename: str
+ source_filename: Optional[str]
+ trace: bool
+ reason: str
+ file_tracer: Optional[FileTracer]
+ has_dynamic_filename: bool
+
+
+# When collecting data, we use a dictionary with a few possible shapes. The
+# keys are always file names.
+# - If measuring line coverage, the values are sets of line numbers.
+# - If measuring arcs in the Python tracer, the values are sets of arcs (pairs
+# of line numbers).
+# - If measuring arcs in the C tracer, the values are sets of packed arcs (two
+# line numbers combined into one integer).
+
+TTraceFileData = Union[Set[TLineNo], Set[TArc], Set[int]]
+
+TTraceData = Dict[str, TTraceFileData]
+
+class TTracer(Protocol):
+ """Either CTracer or PyTracer."""
+
+ data: TTraceData
+ trace_arcs: bool
+ should_trace: Callable[[str, FrameType], TFileDisposition]
+ should_trace_cache: Mapping[str, Optional[TFileDisposition]]
+ should_start_context: Optional[Callable[[FrameType], Optional[str]]]
+ switch_context: Optional[Callable[[Optional[str]], None]]
+ warn: TWarnFn
+
+ def __init__(self) -> None:
+ ...
+
+ def start(self) -> TTraceFn:
+ """Start this tracer, returning a trace function."""
+
+ def stop(self) -> None:
+ """Stop this tracer."""
+
+ def activity(self) -> bool:
+ """Has there been any activity?"""
+
+ def reset_activity(self) -> None:
+ """Reset the activity() flag."""
+
+ def get_stats(self) -> Optional[Dict[str, int]]:
+ """Return a dictionary of statistics, or None."""
+
+## Coverage
+
+# Many places use kwargs as Coverage kwargs.
+TCovKwargs = Any
+
+
+## Configuration
+
+# One value read from a config file.
+TConfigValueIn = Optional[Union[bool, int, float, str, Iterable[str]]]
+TConfigValueOut = Optional[Union[bool, int, float, str, List[str]]]
+# An entire config section, mapping option names to values.
+TConfigSectionIn = Mapping[str, TConfigValueIn]
+TConfigSectionOut = Mapping[str, TConfigValueOut]
+
+class TConfigurable(Protocol):
+ """Something that can proxy to the coverage configuration settings."""
+
+ def get_option(self, option_name: str) -> Optional[TConfigValueOut]:
+ """Get an option from the configuration.
+
+ `option_name` is a colon-separated string indicating the section and
+ option name. For example, the ``branch`` option in the ``[run]``
+ section of the config file would be indicated with `"run:branch"`.
+
+ Returns the value of the option.
+
+ """
+
+ def set_option(self, option_name: str, value: Union[TConfigValueIn, TConfigSectionIn]) -> None:
+ """Set an option in the configuration.
+
+ `option_name` is a colon-separated string indicating the section and
+ option name. For example, the ``branch`` option in the ``[run]``
+ section of the config file would be indicated with `"run:branch"`.
+
+ `value` is the new value for the option.
+
+ """
+
+class TPluginConfig(Protocol):
+ """Something that can provide options to a plugin."""
+
+ def get_plugin_options(self, plugin: str) -> TConfigSectionOut:
+ """Get the options for a plugin."""
+
+
+## Parsing
+
+TMorf = Union[ModuleType, str]
+
+TSourceTokenLines = Iterable[List[Tuple[str, str]]]
+
+## Plugins
+
+class TPlugin(Protocol):
+ """What all plugins have in common."""
+ _coverage_plugin_name: str
+ _coverage_enabled: bool
+
+
+## Debugging
+
+class TWarnFn(Protocol):
+ """A callable warn() function."""
+ def __call__(self, msg: str, slug: Optional[str] = None, once: bool = False) -> None:
+ ...
+
+
+class TDebugCtl(Protocol):
+ """A DebugControl object, or something like it."""
+
+ def should(self, option: str) -> bool:
+ """Decide whether to output debug information in category `option`."""
+
+ def write(self, msg: str) -> None:
+ """Write a line of debug output."""
+
+
+class TWritable(Protocol):
+ """Anything that can be written to."""
+
+ def write(self, msg: str) -> None:
+ """Write a message."""
diff -Nru python-coverage-6.5.0+dfsg1/coverage/version.py python-coverage-7.2.7+dfsg1/coverage/version.py
--- python-coverage-6.5.0+dfsg1/coverage/version.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/version.py 2023-05-29 19:46:30.000000000 +0000
@@ -4,28 +4,47 @@
"""The version and URL for coverage.py"""
# This file is exec'ed in setup.py, don't import anything!
-# Same semantics as sys.version_info.
-version_info = (6, 5, 0, "final", 0)
+from __future__ import annotations
-
-def _make_version(major, minor, micro, releaselevel, serial):
+# version_info: same semantics as sys.version_info.
+# _dev: the .devN suffix if any.
+version_info = (7, 2, 7, "final", 0)
+_dev = 0
+
+
+def _make_version(
+ major: int,
+ minor: int,
+ micro: int,
+ 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']
+ 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]
+ if releaselevel != "final":
+ short = {"alpha": "a", "beta": "b", "candidate": "rc"}[releaselevel]
version += f"{short}{serial}"
+ if dev != 0:
+ version += f".dev{dev}"
return version
-def _make_url(major, minor, micro, releaselevel, serial):
+def _make_url(
+ major: int,
+ minor: int,
+ micro: int,
+ releaselevel: str,
+ serial: int = 0,
+ dev: int = 0,
+) -> str:
"""Make the URL people should start at for this version of coverage.py."""
- url = "https://coverage.readthedocs.io"
- if releaselevel != 'final':
- # For pre-releases, use a version-specific URL.
- url += "/en/" + _make_version(major, minor, micro, releaselevel, serial)
- return url
+ return (
+ "https://coverage.readthedocs.io/en/"
+ + _make_version(major, minor, micro, releaselevel, serial, dev)
+ )
-__version__ = _make_version(*version_info)
-__url__ = _make_url(*version_info)
+__version__ = _make_version(*version_info, _dev)
+__url__ = _make_url(*version_info, _dev)
diff -Nru python-coverage-6.5.0+dfsg1/coverage/xmlreport.py python-coverage-7.2.7+dfsg1/coverage/xmlreport.py
--- python-coverage-6.5.0+dfsg1/coverage/xmlreport.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage/xmlreport.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,28 +3,55 @@
"""XML reporting for coverage.py"""
+from __future__ import annotations
+
import os
import os.path
import sys
import time
import xml.dom.minidom
-from coverage import __url__, __version__, files
+from dataclasses import dataclass
+from typing import Any, Dict, IO, Iterable, Optional, TYPE_CHECKING
+
+from coverage import __version__, files
from coverage.misc import isolate_module, human_sorted, human_sorted_items
-from coverage.report import get_analysis_to_report
+from coverage.plugin import FileReporter
+from coverage.report_core import get_analysis_to_report
+from coverage.results import Analysis
+from coverage.types import TMorf
+from coverage.version import __url__
+
+if TYPE_CHECKING:
+ from coverage import Coverage
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, num):
+def rate(hit: int, num: int) -> str:
"""Return the fraction of `hit`/`num`, as a string."""
if num == 0:
return "1"
else:
- return "%.4g" % (float(hit) / num)
+ return "%.4g" % (hit / num)
+
+
+@dataclass
+class PackageData:
+ """Data we keep about each "package" (in Java terms)."""
+ elements: Dict[str, xml.dom.minidom.Element]
+ hits: int
+ lines: int
+ br_hits: int
+ branches: int
+
+
+def appendChild(parent: Any, child: Any) -> None:
+ """Append a child to a parent, in a way mypy will shut up about."""
+ parent.appendChild(child)
class XmlReporter:
@@ -32,7 +59,7 @@
report_type = "XML report"
- def __init__(self, coverage):
+ def __init__(self, coverage: Coverage) -> None:
self.coverage = coverage
self.config = self.coverage.config
@@ -40,13 +67,15 @@
if self.config.source:
for src in self.config.source:
if os.path.exists(src):
- if not self.config.relative_files:
+ if self.config.relative_files:
+ src = src.rstrip(r"\/")
+ else:
src = files.canonical_filename(src)
self.source_paths.add(src)
- self.packages = {}
- self.xml_out = None
+ self.packages: Dict[str, PackageData] = {}
+ self.xml_out: xml.dom.minidom.Document
- def report(self, morfs, outfile=None):
+ def report(self, morfs: Optional[Iterable[TMorf]], outfile: Optional[IO[str]] = None) -> float:
"""Generate a Cobertura-compatible XML report for `morfs`.
`morfs` is a list of modules or file names.
@@ -60,6 +89,7 @@
# 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)
# Write header stuff.
@@ -81,9 +111,9 @@
# Populate the XML DOM with the source info.
for path in human_sorted(self.source_paths):
xsource = self.xml_out.createElement("source")
- xsources.appendChild(xsource)
+ appendChild(xsources, xsource)
txt = self.xml_out.createTextNode(path)
- xsource.appendChild(txt)
+ appendChild(xsource, txt)
lnum_tot, lhits_tot = 0, 0
bnum_tot, bhits_tot = 0, 0
@@ -93,26 +123,25 @@
# Populate the XML DOM with the package info.
for pkg_name, pkg_data in human_sorted_items(self.packages.items()):
- class_elts, lhits, lnum, bhits, bnum = pkg_data
xpackage = self.xml_out.createElement("package")
- xpackages.appendChild(xpackage)
+ appendChild(xpackages, xpackage)
xclasses = self.xml_out.createElement("classes")
- xpackage.appendChild(xclasses)
- for _, class_elt in human_sorted_items(class_elts.items()):
- xclasses.appendChild(class_elt)
- xpackage.setAttribute("name", pkg_name.replace(os.sep, '.'))
- xpackage.setAttribute("line-rate", rate(lhits, lnum))
+ 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))
if has_arcs:
- branch_rate = rate(bhits, bnum)
+ branch_rate = rate(pkg_data.br_hits, pkg_data.branches)
else:
branch_rate = "0"
xpackage.setAttribute("branch-rate", branch_rate)
xpackage.setAttribute("complexity", "0")
- lnum_tot += lnum
- lhits_tot += lhits
- bnum_tot += bnum
- bhits_tot += bhits
+ 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))
@@ -138,37 +167,38 @@
pct = 100.0 * (lhits_tot + bhits_tot) / denom
return pct
- def xml_file(self, fr, analysis, has_arcs):
+ def xml_file(self, fr: FileReporter, analysis: Analysis, has_arcs: bool) -> None:
"""Add to the XML report for a single file."""
if self.config.skip_empty:
if analysis.numbers.n_statements == 0:
return
- # Create the 'lines' and 'package' XML elements, which
+ # Create the "lines" and "package" XML elements, which
# are populated later. Note that a package == a directory.
filename = fr.filename.replace("\\", "/")
for source_path in self.source_paths:
- source_path = files.canonical_filename(source_path)
+ 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:]
break
else:
- rel_name = fr.relative_filename()
+ 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("/", ".")
- package = self.packages.setdefault(package_name, [{}, 0, 0, 0, 0])
+ package = self.packages.setdefault(package_name, PackageData({}, 0, 0, 0, 0))
- xclass = self.xml_out.createElement("class")
+ xclass: xml.dom.minidom.Element = self.xml_out.createElement("class")
- xclass.appendChild(self.xml_out.createElement("methods"))
+ appendChild(xclass, self.xml_out.createElement("methods"))
xlines = self.xml_out.createElement("lines")
- xclass.appendChild(xlines)
+ appendChild(xclass, xlines)
xclass.setAttribute("name", os.path.relpath(rel_name, dirname))
xclass.setAttribute("filename", rel_name.replace("\\", "/"))
@@ -177,7 +207,7 @@
branch_stats = analysis.branch_stats()
missing_branch_arcs = analysis.missing_branch_arcs()
- # For each statement, create an XML 'line' element.
+ # 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))
@@ -197,7 +227,7 @@
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))
- xlines.appendChild(xline)
+ appendChild(xlines, xline)
class_lines = len(analysis.statements)
class_hits = class_lines - len(analysis.missing)
@@ -207,8 +237,8 @@
missing_branches = sum(t - k for t, k in branch_stats.values())
class_br_hits = class_branches - missing_branches
else:
- class_branches = 0.0
- class_br_hits = 0.0
+ class_branches = 0
+ class_br_hits = 0
# Finalize the statistics that are collected in the XML DOM.
xclass.setAttribute("line-rate", rate(class_hits, class_lines))
@@ -218,13 +248,13 @@
branch_rate = "0"
xclass.setAttribute("branch-rate", branch_rate)
- package[0][rel_name] = xclass
- package[1] += class_hits
- package[2] += class_lines
- package[3] += class_br_hits
- package[4] += class_branches
+ package.elements[rel_name] = xclass
+ package.hits += class_hits
+ package.lines += class_lines
+ package.br_hits += class_br_hits
+ package.branches += class_branches
-def serialize_xml(dom):
+def serialize_xml(dom: xml.dom.minidom.Document) -> str:
"""Serialize a minidom node to XML."""
return dom.toprettyxml()
diff -Nru python-coverage-6.5.0+dfsg1/coverage.egg-info/PKG-INFO python-coverage-7.2.7+dfsg1/coverage.egg-info/PKG-INFO
--- python-coverage-6.5.0+dfsg1/coverage.egg-info/PKG-INFO 2022-09-29 16:36:48.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage.egg-info/PKG-INFO 2023-05-29 19:46:41.000000000 +0000
@@ -1,15 +1,16 @@
Metadata-Version: 2.1
Name: coverage
-Version: 6.5.0
+Version: 7.2.7
Summary: Code coverage measurement for Python
Home-page: https://github.com/nedbat/coveragepy
-Author: Ned Batchelder and 161 others
+Author: Ned Batchelder and 213 others
Author-email: ned@nedbatchelder.com
-License: Apache 2.0
-Project-URL: Documentation, https://coverage.readthedocs.io
+License: Apache-2.0
+Project-URL: Documentation, https://coverage.readthedocs.io/en/7.2.7
Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=pypi
Project-URL: Issues, https://github.com/nedbat/coveragepy/issues
-Project-URL: Twitter, https://twitter.com/coveragepy
+Project-URL: Mastodon, https://hachyderm.io/@coveragepy
+Project-URL: Mastodon (nedbat), https://hachyderm.io/@nedbat
Keywords: code coverage testing
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
@@ -22,6 +23,7 @@
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Software Development :: Quality Assurance
@@ -51,8 +53,8 @@
| |test-status| |quality-status| |docs| |metacov|
| |kit| |downloads| |format| |repos|
| |stars| |forks| |contributors|
-| |tidelift| |core-infrastructure| |open-ssf|
-| |sponsor| |twitter-coveragepy| |twitter-nedbat|
+| |core-infrastructure| |open-ssf| |snyk|
+| |tidelift| |sponsor| |mastodon-coveragepy| |mastodon-nedbat|
Coverage.py measures code coverage, typically during test execution. It uses
the code analysis tools and tracing hooks provided in the Python standard
@@ -62,17 +64,23 @@
.. PYVERSIONS
-* CPython 3.7 through 3.11.0 rc2.
-* PyPy3 7.3.8.
+* CPython 3.7 through 3.12.0b1
+* PyPy3 7.3.11.
Documentation is on `Read the Docs`_. Code repository and issue tracker are on
`GitHub`_.
-.. _Read the Docs: https://coverage.readthedocs.io/
+.. _Read the Docs: https://coverage.readthedocs.io/en/7.2.7/
.. _GitHub: https://github.com/nedbat/coveragepy
+**New in 7.x:**
+improved data combining;
+``[run] exclude_also`` setting;
+``report --format=``;
+type annotations.
-**New in 6.x:** dropped support for Python 2.7, 3.5, and 3.6;
+**New in 6.x:**
+dropped support for Python 2.7, 3.5, and 3.6;
write data on SIGTERM;
added support for 3.10 match/case statements.
@@ -99,9 +107,10 @@
Getting Started
---------------
-See the `Quick Start section`_ of the docs.
+Looking to run ``coverage`` on your test suite? See the `Quick Start section`_
+of the docs.
-.. _Quick Start section: https://coverage.readthedocs.io/#quick-start
+.. _Quick Start section: https://coverage.readthedocs.io/en/7.2.7/#quick-start
Change history
@@ -109,7 +118,7 @@
The complete history of changes is on the `change history page`_.
-.. _change history page: https://coverage.readthedocs.io/en/latest/changes.html
+.. _change history page: https://coverage.readthedocs.io/en/7.2.7/changes.html
Code of Conduct
@@ -125,9 +134,10 @@
Contributing
------------
-See the `Contributing section`_ of the docs.
+Found a bug? Want to help improve the code or documentation? See the
+`Contributing section`_ of the docs.
-.. _Contributing section: https://coverage.readthedocs.io/en/latest/contributing.html
+.. _Contributing section: https://coverage.readthedocs.io/en/7.2.7/contributing.html
Security
@@ -155,7 +165,7 @@
:target: https://github.com/nedbat/coveragepy/actions/workflows/quality.yml
:alt: Quality check status
.. |docs| image:: https://readthedocs.org/projects/coverage/badge/?version=latest&style=flat
- :target: https://coverage.readthedocs.io/
+ :target: https://coverage.readthedocs.io/en/7.2.7/
:alt: Documentation
.. |kit| image:: https://badge.fury.io/py/coverage.svg
:target: https://pypi.org/project/coverage/
@@ -193,12 +203,12 @@
.. |contributors| image:: https://img.shields.io/github/contributors/nedbat/coveragepy.svg?logo=github
:target: https://github.com/nedbat/coveragepy/graphs/contributors
:alt: Contributors
-.. |twitter-coveragepy| image:: https://img.shields.io/twitter/follow/coveragepy.svg?label=coveragepy&style=flat&logo=twitter&logoColor=4FADFF
- :target: https://twitter.com/coveragepy
- :alt: coverage.py on Twitter
-.. |twitter-nedbat| image:: https://img.shields.io/twitter/follow/nedbat.svg?label=nedbat&style=flat&logo=twitter&logoColor=4FADFF
- :target: https://twitter.com/nedbat
- :alt: nedbat on Twitter
+.. |mastodon-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&link=https%3A%2F%2Fhachyderm.io%2F%40nedbat&url=https%3A%2F%2Fhachyderm.io%2Fusers%2Fnedbat%2Ffollowers.json&query=totalItems&label=@nedbat
+ :target: https://hachyderm.io/@nedbat
+ :alt: nedbat on Mastodon
+.. |mastodon-coveragepy| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&link=https%3A%2F%2Fhachyderm.io%2F%40coveragepy&url=https%3A%2F%2Fhachyderm.io%2Fusers%2Fcoveragepy%2Ffollowers.json&query=totalItems&label=@coveragepy
+ :target: https://hachyderm.io/@coveragepy
+ :alt: coveragepy on Mastodon
.. |sponsor| image:: https://img.shields.io/badge/%E2%9D%A4-Sponsor%20me-brightgreen?style=flat&logo=GitHub
:target: https://github.com/sponsors/nedbat
:alt: Sponsor me on GitHub
@@ -208,3 +218,6 @@
.. |open-ssf| image:: https://api.securityscorecards.dev/projects/github.com/nedbat/coveragepy/badge
:target: https://deps.dev/pypi/coverage
:alt: OpenSSF Scorecard
+.. |snyk| image:: https://snyk.io/advisor/python/coverage/badge.svg
+ :target: https://snyk.io/advisor/python/coverage
+ :alt: Snyk package health
diff -Nru python-coverage-6.5.0+dfsg1/coverage.egg-info/SOURCES.txt python-coverage-7.2.7+dfsg1/coverage.egg-info/SOURCES.txt
--- python-coverage-6.5.0+dfsg1/coverage.egg-info/SOURCES.txt 2022-09-29 16:36:48.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/coverage.egg-info/SOURCES.txt 2023-05-29 19:46:41.000000000 +0000
@@ -1,4 +1,5 @@
.editorconfig
+.git-blame-ignore-revs
.readthedocs.yml
CHANGES.rst
CONTRIBUTORS.txt
@@ -13,7 +14,6 @@
metacov.ini
pylintrc
pyproject.toml
-setup.cfg
setup.py
tox.ini
.github/CODE_OF_CONDUCT.md
@@ -33,7 +33,7 @@
ci/README.txt
ci/comment_on_fixes.py
ci/download_gha_artifacts.py
-ci/github_releases.py
+ci/ghrel_template.md.j2
ci/parse_relnotes.py
ci/trigger_build_kits.py
coverage/__init__.py
@@ -63,14 +63,17 @@
coverage/phystokens.py
coverage/plugin.py
coverage/plugin_support.py
+coverage/py.typed
coverage/python.py
coverage/pytracer.py
coverage/report.py
+coverage/report_core.py
coverage/results.py
coverage/sqldata.py
-coverage/summary.py
coverage/templite.py
coverage/tomlconfig.py
+coverage/tracer.pyi
+coverage/types.py
coverage/version.py
coverage/xmlreport.py
coverage.egg-info/PKG-INFO
@@ -118,6 +121,7 @@
doc/howitworks.rst
doc/index.rst
doc/install.rst
+doc/migrating.rst
doc/plugins.rst
doc/python-coverage.1.txt
doc/requirements.in
@@ -136,7 +140,6 @@
doc/sample_html/keybd_closed.png
doc/sample_html/keybd_open.png
lab/README.txt
-lab/benchmark.py
lab/bpo_prelude.py
lab/branch_trace.py
lab/branches.py
@@ -153,10 +156,14 @@
lab/parser.py
lab/platform_info.py
lab/run_trace.py
+lab/select_contexts.py
lab/show_ast.py
lab/show_platform.py
lab/show_pyc.py
lab/treetopy.sh
+lab/benchmark/benchmark.py
+lab/benchmark/empty.py
+lab/benchmark/run.py
lab/notes/bug1303.txt
lab/notes/pypy-738-decorated-functions.txt
requirements/dev.in
@@ -166,6 +173,8 @@
requirements/light-threads.in
requirements/light-threads.pip
requirements/lint.pip
+requirements/mypy.in
+requirements/mypy.pip
requirements/pins.pip
requirements/pip-tools.in
requirements/pip-tools.pip
@@ -217,9 +226,10 @@
tests/test_process.py
tests/test_python.py
tests/test_report.py
+tests/test_report_common.py
+tests/test_report_core.py
tests/test_results.py
tests/test_setup.py
-tests/test_summary.py
tests/test_templite.py
tests/test_testing.py
tests/test_venv.py
@@ -246,6 +256,8 @@
tests/gold/html/b_branch/index.html
tests/gold/html/bom/bom_py.html
tests/gold/html/bom/index.html
+tests/gold/html/contexts/index.html
+tests/gold/html/contexts/two_tests_py.html
tests/gold/html/isolatin1/index.html
tests/gold/html/isolatin1/isolatin1_py.html
tests/gold/html/omit_1/index.html
diff -Nru python-coverage-6.5.0+dfsg1/debian/changelog python-coverage-7.2.7+dfsg1/debian/changelog
--- python-coverage-6.5.0+dfsg1/debian/changelog 2023-07-21 04:59:32.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/debian/changelog 2023-07-23 03:49:58.000000000 +0000
@@ -1,3 +1,20 @@
+python-coverage (7.2.7+dfsg1-1) unstable; urgency=medium
+
+ * The “Adele Puglisi” release.
+ * Note that release 6.5.0+dfsg1-3 Closes: bug#1040850.
+ The previous changelog entry gave the incorrect bug report number.
+ * New upstream version.
+ Highlights since previous release:
+ * Changes to filename pattern matching and path remapping.
+ These might require changing configuration for Coverage.py.
+ See the “Migrating between versions” section of the documentation.
+ * New output formats for ‘report’.
+ * Empty source file no longer fails with ‘--fail-under’.
+ * Filename parameters in API now also accept a ‘pathlib.Path’ object.
+ * Refresh patches for new upstream version.
+
+ -- Ben Finney Sun, 23 Jul 2023 13:49:58 +1000
+
python-coverage (6.5.0+dfsg1-3) unstable; urgency=medium
* Declare conformance to “Standards-Version: 4.6.2”.
diff -Nru python-coverage-6.5.0+dfsg1/debian/copyright python-coverage-7.2.7+dfsg1/debian/copyright
--- python-coverage-6.5.0+dfsg1/debian/copyright 2023-07-21 04:59:32.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/debian/copyright 2023-07-23 03:49:58.000000000 +0000
@@ -10,7 +10,7 @@
Files: *
Copyright:
- © 2004–2022 Ned Batchelder
+ © 2004–2023 Ned Batchelder
© 2001 Gareth Rees
License: Apache-2
License-Grant:
diff -Nru python-coverage-6.5.0+dfsg1/debian/patches/01.omit-resource-files-from-distutils-setup.patch python-coverage-7.2.7+dfsg1/debian/patches/01.omit-resource-files-from-distutils-setup.patch
--- python-coverage-6.5.0+dfsg1/debian/patches/01.omit-resource-files-from-distutils-setup.patch 2023-07-21 04:59:32.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/debian/patches/01.omit-resource-files-from-distutils-setup.patch 2023-07-23 03:49:58.000000000 +0000
@@ -5,21 +5,19 @@
of FHS, and these files should instead go to ‘/usr/share/…’.
Bug-Debian: http://bugs.debian.org/721676
Author: Ben Finney
-Last-Update: 2017-09-08
+Last-Update: 2032-07-23
-diff --git a/setup.py b/setup.py
-index 7962c44d..6ac78b0a 100644
---- a/setup.py
-+++ b/setup.py
-@@ -76,7 +76,6 @@ setup_args = dict(
+diff --git old/setup.py new/setup.py
+--- old/setup.py
++++ new/setup.py
+@@ -87,7 +87,6 @@ setup_args = dict(
package_data={
'coverage': [
- 'htmlfiles/*.*',
'fullcoverage/*.*',
+ 'py.typed',
]
- },
-
Local variables:
coding: utf-8
diff -Nru python-coverage-6.5.0+dfsg1/debian/patches/02.rename-public-programs.patch python-coverage-7.2.7+dfsg1/debian/patches/02.rename-public-programs.patch
--- python-coverage-6.5.0+dfsg1/debian/patches/02.rename-public-programs.patch 2023-07-21 04:59:32.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/debian/patches/02.rename-public-programs.patch 2023-07-23 03:49:58.000000000 +0000
@@ -6,12 +6,12 @@
Created to work with “entry points” feature of Python's Distutils.
Bug: https://bitbucket.org/ned/coveragepy/issue/272/
Author: Ben Finney
-Last-Update: 2022-07-09
+Last-Update: 2023-07-23
diff --git old/setup.py new/setup.py
--- old/setup.py
+++ new/setup.py
-@@ -97,12 +97,11 @@ setup_args = dict(
+@@ -93,12 +93,11 @@ setup_args = dict(
},
entry_points={
@@ -32,15 +32,16 @@
diff --git old/tests/coveragetest.py new/tests/coveragetest.py
--- old/tests/coveragetest.py
+++ new/tests/coveragetest.py
-@@ -323,7 +323,7 @@ class CoverageTest(
+@@ -380,7 +380,7 @@ class CoverageTest(
# their new command name to the tests. This is here for them to override,
# for example:
# https://salsa.debian.org/debian/pkg-python-coverage/-/blob/master/debian/patches/02.rename-public-programs.patch
- coverage_command = "coverage"
+ coverage_command = "python3-coverage"
- def run_command(self, cmd):
+ def run_command(self, cmd: str) -> str:
"""Run the command-line `cmd` in a sub-process.
+
Local variables:
coding: utf-8
diff -Nru python-coverage-6.5.0+dfsg1/doc/changes.rst python-coverage-7.2.7+dfsg1/doc/changes.rst
--- python-coverage-6.5.0+dfsg1/doc/changes.rst 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/changes.rst 2023-05-29 19:46:30.000000000 +0000
@@ -6,7 +6,7 @@
.. The recent changes from the top-level file:
.. include:: ../CHANGES.rst
- :end-before: endchangesinclude
+ :end-before: scriv-end-here
.. Older changes here:
@@ -383,7 +383,7 @@
argument, `no_disk` (default: False). Setting it to True prevents writing
any data to the disk. This is useful for transient data objects.
-- Added the classmethod :meth:`.Coverage.current` to get the latest started
+- Added the class method :meth:`.Coverage.current` to get the latest started
Coverage instance.
- Multiprocessing support in Python 3.8 was broken, but is now fixed. Closes
@@ -556,7 +556,7 @@
- Development moved from `Bitbucket`_ to `GitHub`_.
-- HTML files no longer have trailing and extra whitespace.
+- HTML files no longer have trailing and extra white space.
- The sort order in the HTML report is stored in local storage rather than
cookies, closing `issue 611`_. Thanks, Federico Bond.
@@ -794,7 +794,7 @@
also continue measurement. Both `issue 79`_ and `issue 448`_ described this
problem, and have been fixed.
-- Plugins can now find unexecuted files if they choose, by implementing the
+- Plugins can now find un-executed files if they choose, by implementing the
`find_executable_files` method. Thanks, Emil Madsen.
- Minimal IronPython support. You should be able to run IronPython programs
@@ -1202,7 +1202,7 @@
- The XML report now produces correct package names for modules found in
directories specified with ``source=``. Fixes `issue 465`_.
-- ``coverage report`` won't produce trailing whitespace.
+- ``coverage report`` won't produce trailing white space.
.. _issue 465: https://github.com/nedbat/coveragepy/issues/465
.. _issue 466: https://github.com/nedbat/coveragepy/issues/466
@@ -1532,7 +1532,7 @@
- Files with incorrect encoding declaration comments are no longer ignored by
the reporting commands, fixing `issue 351`_.
-- HTML reports now include a timestamp in the footer, closing `issue 299`_.
+- HTML reports now include a time stamp in the footer, closing `issue 299`_.
Thanks, Conrad Ho.
- HTML reports now begrudgingly use double-quotes rather than single quotes,
@@ -1685,7 +1685,7 @@
`issue 328`_. Thanks, Buck Evan.
- The regex for matching exclusion pragmas has been fixed to allow more kinds
- of whitespace, fixing `issue 334`_.
+ of white space, fixing `issue 334`_.
- Made some PyPy-specific tweaks to improve speed under PyPy. Thanks, Alex
Gaynor.
@@ -1739,7 +1739,7 @@
`issue 285`_. Thanks, Chris Rose.
- HTML reports no longer raise UnicodeDecodeError if a Python file has
- undecodable characters, fixing `issue 303`_ and `issue 331`_.
+ un-decodable characters, fixing `issue 303`_ and `issue 331`_.
- The annotate command will now annotate all files, not just ones relative to
the current directory, fixing `issue 57`_.
@@ -1791,7 +1791,7 @@
- Coverage.py properly supports .pyw files, fixing `issue 261`_.
- Omitting files within a tree specified with the ``source`` option would
- cause them to be incorrectly marked as unexecuted, as described in
+ cause them to be incorrectly marked as un-executed, as described in
`issue 218`_. This is now fixed.
- When specifying paths to alias together during data combining, you can now
@@ -1802,7 +1802,7 @@
(``build/$BUILDNUM/src``).
- Trying to create an XML report with no files to report on, would cause a
- ZeroDivideError, but no longer does, fixing `issue 250`_.
+ ZeroDivisionError, but no longer does, fixing `issue 250`_.
- When running a threaded program under the Python tracer, coverage.py no
longer issues a spurious warning about the trace function changing: "Trace
@@ -1905,7 +1905,7 @@
Thanks, Marcus Cobden.
- Coverage percentage metrics are now computed slightly differently under
- branch coverage. This means that completely unexecuted files will now
+ branch coverage. This means that completely un-executed files will now
correctly have 0% coverage, fixing `issue 156`_. This also means that your
total coverage numbers will generally now be lower if you are measuring
branch coverage.
@@ -2068,7 +2068,7 @@
- Now the exit status of your product code is properly used as the process
status when running ``python -m coverage run ...``. Thanks, JT Olds.
-- When installing into pypy, we no longer attempt (and fail) to compile
+- When installing into PyPy, we no longer attempt (and fail) to compile
the C tracer function, closing `issue 166`_.
.. _issue 142: https://github.com/nedbat/coveragepy/issues/142
@@ -2234,9 +2234,10 @@
Version 3.4b2 — 2010-09-06
--------------------------
-- Completely unexecuted files can now be included in coverage results, reported
- as 0% covered. This only happens if the --source option is specified, since
- coverage.py needs guidance about where to look for source files.
+- Completely un-executed files can now be included in coverage results,
+ reported as 0% covered. This only happens if the --source option is
+ specified, since coverage.py needs guidance about where to look for source
+ files.
- The XML report output now properly includes a percentage for branch coverage,
fixing `issue 65`_ and `issue 81`_.
@@ -2374,7 +2375,7 @@
`config_file=False`.
- Fixed a problem with nested loops having their branch possibilities
- mischaracterized: `issue 39`_.
+ mis-characterized: `issue 39`_.
- Added coverage.process_start to enable coverage measurement when Python
starts.
diff -Nru python-coverage-6.5.0+dfsg1/doc/cmd.rst python-coverage-7.2.7+dfsg1/doc/cmd.rst
--- python-coverage-6.5.0+dfsg1/doc/cmd.rst 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/cmd.rst 2023-05-29 19:46:30.000000000 +0000
@@ -6,6 +6,12 @@
Running "make prebuild" will bring it up to date.
.. [[[cog
+ # optparse wraps help to the COLUMNS value. Set it here to be sure it's
+ # consistent regardless of the environment. Has to be set before we
+ # import cmdline.py, which creates the optparse objects.
+ import os
+ os.environ["COLUMNS"] = "80"
+
import contextlib
import io
import re
@@ -342,7 +348,7 @@
$ coverage combine
-You can also name directories or files on the command line::
+You can also name directories or files to be combined on the command line::
$ coverage combine data1.dat windows_data_files/
@@ -364,19 +370,6 @@
runs, use the ``--append`` switch on the **combine** command. This behavior
was the default before version 4.2.
-To combine data for a source file, coverage has to find its data in each of the
-data files. Different test runs may run the same source file from different
-locations. For example, different operating systems will use different paths
-for the same file, or perhaps each Python version is run from a different
-subdirectory. Coverage needs to know that different file paths are actually
-the same source file for reporting purposes.
-
-You can tell coverage.py how different source locations relate with a
-``[paths]`` section in your configuration file (see :ref:`config_paths`).
-It might be more convenient to use the ``[run] relative_files``
-setting to store relative file paths (see :ref:`relative_files
-`).
-
If any of the data files can't be read, coverage.py will print a warning
indicating the file and the problem.
@@ -389,11 +382,10 @@
$ coverage combine --help
Usage: coverage combine [options] ...
- Combine data from multiple coverage files collected with 'run -p'. 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.
+ 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.
Options:
-a, --append Append coverage data to .coverage, otherwise it starts
@@ -409,7 +401,29 @@
--rcfile=RCFILE Specify configuration file. By default '.coveragerc',
'setup.cfg', 'tox.ini', and 'pyproject.toml' are
tried. [env: COVERAGE_RCFILE]
-.. [[[end]]] (checksum: 0ac91b0781d7146b87953f09090dab92)
+.. [[[end]]] (checksum: 0bdd83f647ee76363c955bedd9ddf749)
+
+
+.. _cmd_combine_remapping:
+
+Re-mapping paths
+................
+
+To combine data for a source file, coverage has to find its data in each of the
+data files. Different test runs may run the same source file from different
+locations. For example, different operating systems will use different paths
+for the same file, or perhaps each Python version is run from a different
+subdirectory. Coverage needs to know that different file paths are actually
+the same source file for reporting purposes.
+
+You can tell coverage.py how different source locations relate with a
+``[paths]`` section in your configuration file (see :ref:`config_paths`).
+It might be more convenient to use the ``[run] relative_files``
+setting to store relative file paths (see :ref:`relative_files
+`).
+
+If data isn't combining properly, you can see details about the inner workings
+with ``--debug=pathmap``.
.. _cmd_erase:
@@ -510,6 +524,8 @@
file. Defaults to '.coverage'. [env: COVERAGE_FILE]
--fail-under=MIN Exit with a status of 2 if the total coverage is less
than MIN.
+ --format=FORMAT Output format, either text (default), markdown, or
+ total.
-i, --ignore-errors Ignore errors while reading source files.
--include=PAT1,PAT2,...
Include only files whose paths match one of these
@@ -532,7 +548,7 @@
--rcfile=RCFILE Specify configuration file. By default '.coveragerc',
'setup.cfg', 'tox.ini', and 'pyproject.toml' are
tried. [env: COVERAGE_RCFILE]
-.. [[[end]]] (checksum: 2f8dde61bab2f44fbfe837aeae87dfd2)
+.. [[[end]]] (checksum: 167272a29d9e7eb017a592a0e0747a06)
The ``-m`` flag also shows the line numbers of missing statements::
@@ -583,6 +599,12 @@
The ``--sort`` option is the name of a column to sort the report by.
+The ``--format`` option controls the style of the report. ``--format=text``
+creates plain text tables as shown above. ``--format=markdown`` creates
+Markdown tables. ``--format=total`` writes out a single number, the total
+coverage percentage as shown at the end of the tables, but without a percent
+sign.
+
Other common reporting options are described above in :ref:`cmd_reporting`.
These options can also be set in your .coveragerc file. See
:ref:`Configuration: [report] `.
@@ -602,9 +624,10 @@
__ https://nedbatchelder.com/files/sample_coverage_html/index.html
-Lines are highlighted green for executed, red for missing, and gray for
-excluded. The counts at the top of the file are buttons to turn on and off
-the highlighting.
+Lines are highlighted: green for executed, red for missing, and gray for
+excluded. If you've used branch coverage, partial branches are yellow. The
+colored counts at the top of the file are buttons to turn on and off the
+highlighting.
A number of keyboard shortcuts are available for navigating the report.
Click the keyboard icon in the upper right to see the complete list.
@@ -1001,7 +1024,7 @@
* ``multiproc``: log the start and stop of multiprocessing processes.
* ``pathmap``: log the remapping of paths that happens during ``coverage
- combine`` due to the ``[paths]`` setting. See :ref:`config_paths`.
+ combine``. See :ref:`config_paths`.
* ``pid``: annotate all warnings and debug output with the process and thread
ids.
@@ -1009,7 +1032,8 @@
* ``plugin``: print information about plugin operations.
* ``process``: show process creation information, and changes in the current
- directory.
+ directory. This also writes a time stamp and command arguments into the data
+ file.
* ``pybehave``: show the values of `internal flags `_ describing the
behavior of the current version of Python.
@@ -1033,7 +1057,9 @@
a comma-separated list of these options, or in the :ref:`config_run_debug`
section of the .coveragerc file.
-The debug output goes to stderr, unless the ``COVERAGE_DEBUG_FILE`` environment
-variable names a different file, which will be appended to.
-``COVERAGE_DEBUG_FILE`` accepts the special names ``stdout`` and ``stderr`` to
-write to those destinations.
+The debug output goes to stderr, unless the :ref:`config_run_debug_file`
+setting or the ``COVERAGE_DEBUG_FILE`` environment variable names a different
+file, which will be appended to. This can be useful because many test runners
+capture output, which could hide important details. ``COVERAGE_DEBUG_FILE``
+accepts the special names ``stdout`` and ``stderr`` to write to those
+destinations.
diff -Nru python-coverage-6.5.0+dfsg1/doc/config.rst python-coverage-7.2.7+dfsg1/doc/config.rst
--- python-coverage-6.5.0+dfsg1/doc/config.rst 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/config.rst 2023-05-29 19:46:30.000000000 +0000
@@ -31,10 +31,14 @@
configuration file is used. It will automatically read from "setup.cfg" or
"tox.ini" if they exist. In this case, the section names have "coverage:"
prefixed, so the ``[run]`` options described below will be found in the
-``[coverage:run]`` section of the file. If coverage.py is installed with the
-``toml`` extra (``pip install coverage[toml]``), it will automatically read
-from "pyproject.toml". Configuration must be within the ``[tool.coverage]``
-section, for example, ``[tool.coverage.run]``.
+``[coverage:run]`` section of the file.
+
+Coverage.py will read from "pyproject.toml" if TOML support is available,
+either because you are running on Python 3.11 or later, or because you
+installed with the ``toml`` extra (``pip install coverage[toml]``).
+Configuration must be within the ``[tool.coverage]`` section, for example,
+``[tool.coverage.run]``. Environment variable expansion in values is
+available, but only within quoted strings, even for non-string values.
Syntax
@@ -75,10 +79,7 @@
[report]
# Regexes for lines to exclude from consideration
- exclude_lines =
- # Have to re-enable the standard pragma
- pragma: no cover
-
+ exclude_also =
# Don't complain about missing debug-only code:
def __repr__
if self\.debug
@@ -199,6 +200,15 @@
` for details.
+.. _config_run_debug_file:
+
+[run] debug_file
+................
+
+(string) A file name to write debug output to. See :ref:`the run --debug
+option ` for details.
+
+
.. _config_run_dynamic_context:
[run] dynamic_context
@@ -218,14 +228,6 @@
details.
-.. _config_run_note:
-
-[run] note
-..........
-
-(string) This is now obsolete.
-
-
.. _config_run_omit:
[run] omit
@@ -259,9 +261,9 @@
[run] relative_files
....................
-(*experimental*, boolean, default False) store relative file paths in the data
-file. This makes it easier to measure code in one (or multiple) environments,
-and then report in another. See :ref:`cmd_combine` for details.
+(boolean, default False) store relative file paths in the data file. This
+makes it easier to measure code in one (or multiple) environments, and then
+report in another. See :ref:`cmd_combine` for details.
Note that setting ``source`` has to be done in the configuration file rather
than the command line for this option to work, since the reporting commands
@@ -348,12 +350,18 @@
against the source file found at "src/module.py".
If you specify more than one list of paths, they will be considered in order.
-The first list that has a match will be used.
+A file path will only be remapped if the result exists. If a path matches a
+list, but the result doesn't exist, the next list will be tried. The first
+list that has an existing result will be used.
+
+Remapping will also be done during reporting, but only within the single data
+file being reported. Combining multiple files requires the ``combine``
+command.
The ``--debug=pathmap`` option can be used to log details of the re-mapping of
paths. See :ref:`the --debug option `.
-See :ref:`cmd_combine` for more information.
+See :ref:`cmd_combine_remapping` and :ref:`source_glob` for more information.
.. _config_report:
@@ -364,16 +372,31 @@
Settings common to many kinds of reporting.
+.. _config_report_exclude_also:
+
+[report] exclude_also
+.....................
+
+(multi-string) A list of regular expressions. This setting is similar to
+:ref:`config_report_exclude_lines`: it specifies patterns for lines to exclude
+from reporting. This setting is preferred, because it will preserve the
+default exclude patterns instead of overwriting them.
+
+.. versionadded:: 7.2.0
+
+
.. _config_report_exclude_lines:
[report] exclude_lines
......................
(multi-string) A list of regular expressions. Any line of your source code
-containing a match for one of these regexes is excluded from being reported as
+containing a match for one of these regexes is excluded from being reported as
missing. More details are in :ref:`excluding`. If you use this option, you
are replacing all the exclude regexes, so you'll need to also supply the
-"pragma: no cover" regex if you still want to use it.
+"pragma: no cover" regex if you still want to use it. The
+:ref:`config_report_exclude_also` setting can be used to specify patterns
+without overwriting the default set.
You can exclude lines introducing blocks, and the entire block is excluded. If
you exclude a ``def`` line or decorator line, the entire function is excluded.
@@ -389,7 +412,7 @@
[report] fail_under
...................
-(float) A target coverage percentage. If the total coverage measurement is
+(float) A target coverage percentage. If the total coverage measurement is
under this value, then exit with a status code of 2. If you specify a
non-integral value, you must also set ``[report] precision`` properly to make
use of the decimal places. A setting of 100 will fail any value under 100,
@@ -414,6 +437,20 @@
See :ref:`source` for details.
+.. _config_include_namespace_packages:
+
+[report] include_namespace_packages
+...................................
+
+(boolean, default False) When searching for completely un-executed files,
+include directories without ``__init__.py`` files. These are `implicit
+namespace packages`_, and are usually skipped.
+
+.. _implicit namespace packages: https://peps.python.org/pep-0420/
+
+.. versionadded:: 7.0
+
+
.. _config_report_omit:
[report] omit
@@ -603,7 +640,7 @@
[json] pretty_print
...................
-(boolean, default false) Controls if the JSON is outputted with whitespace
+(boolean, default false) Controls if the JSON is outputted with white space
formatted for human consumption (True) or for minimum file size (False).
diff -Nru python-coverage-6.5.0+dfsg1/doc/conf.py python-coverage-7.2.7+dfsg1/doc/conf.py
--- python-coverage-6.5.0+dfsg1/doc/conf.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/conf.py 2023-05-29 19:46:30.000000000 +0000
@@ -36,13 +36,14 @@
'sphinx.ext.autodoc',
'sphinx.ext.todo',
'sphinx.ext.ifconfig',
- 'sphinxcontrib.spelling',
'sphinx.ext.intersphinx',
'sphinxcontrib.restbuilder',
'sphinx.ext.napoleon',
#'sphinx_tabs.tabs',
]
+autodoc_typehints = "description"
+
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -57,18 +58,20 @@
# General information about the project.
project = 'Coverage.py'
-copyright = '2009\N{EN DASH}2022, Ned Batchelder' # CHANGEME # pylint: disable=redefined-builtin
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
-#
-# The short X.Y.Z version. # CHANGEME
-version = "6.5.0"
-# The full version, including alpha/beta/rc tags. # CHANGEME
-release = "6.5.0"
-# The date of release, in "monthname day, year" format. # CHANGEME
-release_date = "September 29, 2022"
+
+# @@@ editable
+copyright = "2009–2023, Ned Batchelder" # pylint: disable=redefined-builtin
+# The short X.Y.Z version.
+version = "7.2.7"
+# The full version, including alpha/beta/rc tags.
+release = "7.2.7"
+# The date of release, in "monthname day, year" format.
+release_date = "May 29, 2023"
+# @@@ end
rst_epilog = """
.. |release_date| replace:: {release_date}
@@ -121,6 +124,19 @@
'python': ('https://docs.python.org/3', None),
}
+nitpick_ignore = [
+ ("py:class", "frame"),
+ ("py:class", "module"),
+ ("py:class", "DefaultValue"),
+ ("py:class", "FilePath"),
+ ("py:class", "TWarnFn"),
+ ("py:class", "TDebugCtl"),
+]
+
+nitpick_ignore_regex = [
+ (r"py:class", r"coverage\..*\..*"),
+]
+
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
@@ -203,6 +219,9 @@
# -- Spelling ---
if any("spell" in arg for arg in sys.argv):
+ # sphinxcontrib.spelling needs the native "enchant" library, which often is
+ # missing, so only use the extension if we are specifically spell-checking.
+ extensions += ['sphinxcontrib.spelling']
names_file = tempfile.NamedTemporaryFile(mode='w', prefix="coverage_names_", suffix=".txt")
with open("../CONTRIBUTORS.txt") as contributors:
names = set(re.split(r"[^\w']", contributors.read()))
diff -Nru python-coverage-6.5.0+dfsg1/doc/contributing.rst python-coverage-7.2.7+dfsg1/doc/contributing.rst
--- python-coverage-6.5.0+dfsg1/doc/contributing.rst 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/contributing.rst 2023-05-29 19:46:30.000000000 +0000
@@ -37,23 +37,24 @@
https://github.com/nedbat/coveragepy. To get a working environment, follow
these steps:
-.. minimum of PYVERSIONS:
+#. `Fork the repo`_ into your own GitHub account. The coverage.py code will
+ then be copied into a GitHub repository at
+ ``https://github.com/GITHUB_USER/coveragepy`` where GITHUB_USER is your
+ GitHub username.
-#. Create a Python 3.7 virtualenv to work in, and activate it.
+#. (Optional) Create a virtualenv to work in, and activate it. There
+ are a number of ways to do this. Use the method you are comfortable with.
#. Clone the repository::
- $ git clone https://github.com/nedbat/coveragepy
+ $ git clone https://github.com/GITHUB_USER/coveragepy
$ cd coveragepy
#. Install the requirements::
- $ pip install -r requirements/dev.pip
+ $ python3 -m pip install -r requirements/dev.in
-#. Install a number of versions of Python. Coverage.py supports a range
- of Python versions. The more you can test with, the more easily your code
- can be used as-is. If you only have one version, that's OK too, but may
- mean more work integrating your contribution.
+ Note: You may need to upgrade pip to install the requirements.
Running the tests
@@ -62,57 +63,91 @@
The tests are written mostly as standard unittest-style tests, and are run with
pytest running under `tox`_::
- $ tox
- py37 create: /Users/nedbat/coverage/trunk/.tox/py37
- py37 installdeps: -rrequirements/pip.pip, -rrequirements/pytest.pip, eventlet==0.25.1, greenlet==0.4.15
- py37 develop-inst: /Users/nedbat/coverage/trunk
- py37 installed: apipkg==1.5,appdirs==1.4.4,attrs==20.3.0,backports.functools-lru-cache==1.6.4,-e git+git@github.com:nedbat/coveragepy.git@36ef0e03c0439159c2245d38de70734fa08cddb4#egg=coverage,decorator==5.0.7,distlib==0.3.1,dnspython==2.1.0,eventlet==0.25.1,execnet==1.8.0,filelock==3.0.12,flaky==3.7.0,future==0.18.2,greenlet==0.4.15,hypothesis==6.10.1,importlib-metadata==4.0.1,iniconfig==1.1.1,monotonic==1.6,packaging==20.9,pluggy==0.13.1,py==1.10.0,PyContracts @ git+https://github.com/slorg1/contracts@c5a6da27d4dc9985f68e574d20d86000880919c3,pyparsing==2.4.7,pytest==6.2.3,pytest-forked==1.3.0,pytest-xdist==2.2.1,qualname==0.1.0,six==1.15.0,sortedcontainers==2.3.0,toml==0.10.2,typing-extensions==3.10.0.0,virtualenv==20.4.4,zipp==3.4.1
- py37 run-test-pre: PYTHONHASHSEED='376882681'
- py37 run-test: commands[0] | python setup.py --quiet clean develop
- py37 run-test: commands[1] | python igor.py zip_mods remove_extension
- py37 run-test: commands[2] | python igor.py test_with_tracer py
- === CPython 3.7.10 with Python tracer (.tox/py37/bin/python) ===
+ % python3 -m tox
+ ROOT: tox-gh won't override envlist because tox is not running in GitHub Actions
+ .pkg: _optional_hooks> python /usr/local/virtualenvs/coverage/lib/python3.7/site-packages/pyproject_api/_backend.py True setuptools.build_meta
+ .pkg: get_requires_for_build_editable> python /usr/local/virtualenvs/coverage/lib/python3.7/site-packages/pyproject_api/_backend.py True setuptools.build_meta
+ .pkg: build_editable> python /usr/local/virtualenvs/coverage/lib/python3.7/site-packages/pyproject_api/_backend.py True setuptools.build_meta
+ py37: install_package> python -m pip install -U --force-reinstall --no-deps .tox/.tmp/package/87/coverage-7.2.3a0.dev1-0.editable-cp37-cp37m-macosx_10_15_x86_64.whl
+ py37: commands[0]> python igor.py zip_mods
+ py37: commands[1]> python setup.py --quiet build_ext --inplace
+ py37: commands[2]> python -m pip install -q -e .
+ py37: commands[3]> python igor.py test_with_tracer c
+ === CPython 3.7.15 with C tracer (.tox/py37/bin/python) ===
bringing up nodes...
- ........................................................................................................................................................... [ 15%]
- ........................................................................................................................................................... [ 31%]
- ...........................................................................................................................................s............... [ 47%]
- ...........................................s...................................................................................sss.sssssssssssssssssss..... [ 63%]
- ........................................................................................................................................................s.. [ 79%]
- ......................................s..................................s................................................................................. [ 95%]
- ........................................ss...... [100%]
- 949 passed, 29 skipped in 40.56s
- py37 run-test: commands[3] | python setup.py --quiet build_ext --inplace
- py37 run-test: commands[4] | python igor.py test_with_tracer c
- === CPython 3.7.10 with C tracer (.tox/py37/bin/python) ===
+ .........................................................................................................................x.................s....s....... [ 11%]
+ ..s.....x.............................................s................................................................................................. [ 22%]
+ ........................................................................................................................................................ [ 34%]
+ ........................................................................................................................................................ [ 45%]
+ ........................................................................................................................................................ [ 57%]
+ .........s....................................................................................................................s......................... [ 68%]
+ .................................s..............................s...............s..................................s.................................... [ 80%]
+ ........................................................s............................................................................................... [ 91%]
+ ......................................s......................................................................... [100%]
+ 1316 passed, 12 skipped, 2 xfailed in 36.42s
+ py37: commands[4]> python igor.py remove_extension
+ py37: commands[5]> python igor.py test_with_tracer py
+ === CPython 3.7.15 with Python tracer (.tox/py37/bin/python) ===
bringing up nodes...
- ........................................................................................................................................................... [ 15%]
- ........................................................................................................................................................... [ 31%]
- ......................................................................s.................................................................................... [ 47%]
- ........................................................................................................................................................... [ 63%]
- ..........................s................................................s............................................................................... [ 79%]
- .................................................................................s......................................................................... [ 95%]
- ......................................s......... [100%]
- 973 passed, 5 skipped in 41.36s
- ____________________________________________________________________________ summary _____________________________________________________________________________
- py37: commands succeeded
- congratulations :)
+ ................................................................................................x...........................x.................s......... [ 11%]
+ .....s.............s.s.....................................................s..............ss............................s.ss....ss.ss................... [ 22%]
+ ......................................................................................................................................s................. [ 34%]
+ ..................................................................................................................s..................................... [ 45%]
+ ...................s.ss.....................................................................................s....................s.ss................... [ 57%]
+ ..................s.s................................................................................................................................... [ 68%]
+ ..........................s.........................................ssss...............s.................s...sss..................s...ss...ssss.s....... [ 80%]
+ .......................................................................................................................................................s [ 91%]
+ .........................................................................s.................................ss.... [100%]
+ 1281 passed, 47 skipped, 2 xfailed in 33.86s
+ .pkg: _exit> python /usr/local/virtualenvs/coverage/lib/python3.7/site-packages/pyproject_api/_backend.py True setuptools.build_meta
+ py37: OK (82.38=setup[2.80]+cmd[0.20,0.35,7.30,37.20,0.21,34.32] seconds)
+ congratulations :) (83.61 seconds)
Tox runs the complete test suite twice for each version of Python you have
-installed. The first run uses the Python implementation of the trace function,
-the second uses the C implementation.
+installed. The first run uses the C implementation of the trace function,
+the second uses the Python implementation.
To limit tox to just a few versions of Python, use the ``-e`` switch::
- $ tox -e py37,py39
-
-To run just a few tests, you can use `pytest test selectors`_::
+ $ python3 -m tox -e py37,py39
- $ tox tests/test_misc.py
- $ tox tests/test_misc.py::HasherTest
- $ tox tests/test_misc.py::HasherTest::test_string_hashing
+On the tox command line, options after ``--`` are passed to pytest. To run
+just a few tests, you can use `pytest test selectors`_::
-These command run the tests in one file, one class, and just one test,
-respectively.
+ $ python3 -m tox -- tests/test_misc.py
+ $ python3 -m tox -- tests/test_misc.py::HasherTest
+ $ python3 -m tox -- tests/test_misc.py::HasherTest::test_string_hashing
+
+These commands run the tests in one file, one class, and just one test,
+respectively. The pytest ``-k`` option selects tests based on a word in their
+name, which can be very convenient for ad-hoc test selection. Of course you
+can combine tox and pytest options::
+
+ $ python3 -m tox -q -e py37 -- -n 0 -vv -k hash
+ === CPython 3.7.15 with C tracer (.tox/py37/bin/python) ===
+ ======================================= test session starts ========================================
+ platform darwin -- Python 3.7.15, pytest-7.2.2, pluggy-1.0.0 -- /Users/nedbat/coverage/.tox/py37/bin/python
+ cachedir: .tox/py37/.pytest_cache
+ rootdir: /Users/nedbat/coverage, configfile: setup.cfg
+ plugins: flaky-3.7.0, hypothesis-6.70.0, xdist-3.2.1
+ collected 1330 items / 1320 deselected / 10 selected
+ run-last-failure: no previously failed tests, not deselecting items.
+
+ tests/test_data.py::CoverageDataTest::test_add_to_hash_with_lines PASSED [ 10%]
+ tests/test_data.py::CoverageDataTest::test_add_to_hash_with_arcs PASSED [ 20%]
+ tests/test_data.py::CoverageDataTest::test_add_to_lines_hash_with_missing_file PASSED [ 30%]
+ tests/test_data.py::CoverageDataTest::test_add_to_arcs_hash_with_missing_file PASSED [ 40%]
+ tests/test_execfile.py::RunPycFileTest::test_running_hashed_pyc PASSED [ 50%]
+ tests/test_misc.py::HasherTest::test_string_hashing PASSED [ 60%]
+ tests/test_misc.py::HasherTest::test_bytes_hashing PASSED [ 70%]
+ tests/test_misc.py::HasherTest::test_unicode_hashing PASSED [ 80%]
+ tests/test_misc.py::HasherTest::test_dict_hashing PASSED [ 90%]
+ tests/test_misc.py::HasherTest::test_dict_collision PASSED [100%]
+
+ =============================== 10 passed, 1320 deselected in 1.88s ================================
+ Skipping tests with Python tracer: Only one tracer: no Python tracer for CPython
+ py37: OK (12.22=setup[2.19]+cmd[0.20,0.36,6.57,2.51,0.20,0.19] seconds)
+ congratulations :) (13.10 seconds)
You can also affect the test runs with environment variables. Define any of
these as 1 to use them:
@@ -151,7 +186,8 @@
keep you from sending patches. I can clean them up.
Lines should be kept to a 100-character maximum length. I recommend an
-`editorconfig.org`_ plugin for your editor of choice.
+`editorconfig.org`_ plugin for your editor of choice, which will also help with
+indentation, line endings and so on.
Other style questions are best answered by looking at the existing code.
Formatting of docstrings, comments, long lines, and so on, should match the
@@ -215,6 +251,7 @@
fixes. If you need help writing tests, please ask.
+.. _fork the repo: https://docs.github.com/en/get-started/quickstart/fork-a-repo
.. _editorconfig.org: http://editorconfig.org
.. _tox: https://tox.readthedocs.io/
.. _black: https://pypi.org/project/black/
diff -Nru python-coverage-6.5.0+dfsg1/doc/dbschema.rst python-coverage-7.2.7+dfsg1/doc/dbschema.rst
--- python-coverage-6.5.0+dfsg1/doc/dbschema.rst 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/dbschema.rst 2023-05-29 19:46:30.000000000 +0000
@@ -66,7 +66,7 @@
key text,
value text,
unique (key)
- -- Keys:
+ -- Possible keys:
-- 'has_arcs' boolean -- Is this data recording branches?
-- 'sys_argv' text -- The coverage command line that recorded the data.
-- 'version' text -- The version of coverage.py that made the file.
@@ -116,7 +116,7 @@
foreign key (file_id) references file (id)
);
-.. [[[end]]] (checksum: cfce1df016afbb43a5ff94306db56657)
+.. [[[end]]] (checksum: 6a04d14b07f08f86cccf43056328dcb7)
.. _numbits:
diff -Nru python-coverage-6.5.0+dfsg1/doc/dict.txt python-coverage-7.2.7+dfsg1/doc/dict.txt
--- python-coverage-6.5.0+dfsg1/doc/dict.txt 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/dict.txt 2023-05-29 19:46:30.000000000 +0000
@@ -1,18 +1,36 @@
+API
+BOM
+BTW
+CPython
+CTracer
+Cobertura
+Consolas
+Cython
+DOCTYPE
+DOM
+HTML
+Jinja
+Mako
+OK
+PYTHONPATH
+TODO
+Tidelift
+URL
+UTF
+XML
activestate
-api
apache
-API
+api
args
argv
ascii
+async
basename
basenames
bitbucket
-BOM
bom
boolean
booleans
-BTW
btw
builtin
builtins
@@ -27,7 +45,6 @@
chdir'd
clickable
cmdline
-Cobertura
codecs
colorsys
combinable
@@ -38,17 +55,16 @@
configurability's
configurer
configurers
-Consolas
cov
coveragepy
coveragerc
covhtml
-CPython
css
-CTracer
-Cython
+dataio
datetime
deallocating
+debounce
+decodable
dedent
defaultdict
deserialize
@@ -62,8 +78,6 @@
docstrings
doctest
doctests
-DOCTYPE
-DOM
encodable
encodings
endfor
@@ -75,6 +89,7 @@
execfile
executability
executable's
+execv
expr
extensibility
favicon
@@ -96,10 +111,10 @@
gitignore
globals
greenlet
+hintedness
hotkey
hotkeys
html
-HTML
htmlcov
http
https
@@ -111,15 +126,13 @@
invariants
iterable
iterables
-Jinja
-jquery
jQuery
+jquery
json
jython
kwargs
lcov
localStorage
-Mako
manylinux
matcher
matchers
@@ -136,8 +149,10 @@
morf
morfs
multi
+multiproc
mumbo
mycode
+mypy
namespace
namespaces
nano
@@ -145,13 +160,14 @@
ned
nedbat
nedbatchelder
+newb
+nocover
nosetests
nullary
num
numbits
numpy
ok
-OK
opcode
opcodes
optparse
@@ -161,13 +177,15 @@
parallelizing
parsable
parsers
+pathlib
pathnames
plugin
plugins
pragma
-pragmas
pragma'd
+pragmas
pre
+premain
prepended
prepending
programmability
@@ -175,17 +193,19 @@
py
py's
pyc
+pyenv
pyexpat
+pylib
pylint
pyproject
pypy
pytest
pythonpath
-PYTHONPATH
pyw
rcfile
readme
readthedocs
+realpath
recordable
refactored
refactoring
@@ -194,9 +214,11 @@
regexes
reimplemented
renderer
+rootname
runnable
runtime
scrollbar
+septatrix
serializable
settrace
setuptools
@@ -217,13 +239,10 @@
symlinks
syntaxes
sys
-templite
templating
+templite
testability
-Tidelift
-timestamp
todo
-TODO
tokenization
tokenize
tokenized
@@ -241,7 +260,6 @@
ubuntu
undecodable
unexecutable
-unexecuted
unicode
uninstall
unittest
@@ -249,19 +267,17 @@
unrunnable
unsubscriptable
untokenizable
+usecache
username
-URL
-UTF
utf
vendored
versionadded
virtualenv
-whitespace
wikipedia
wildcard
wildcards
www
+xdist
xml
-XML
xrange
xyzzy
diff -Nru python-coverage-6.5.0+dfsg1/doc/excluding.rst python-coverage-7.2.7+dfsg1/doc/excluding.rst
--- python-coverage-6.5.0+dfsg1/doc/excluding.rst 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/excluding.rst 2023-05-29 19:46:30.000000000 +0000
@@ -80,14 +80,13 @@
all of them by adding a regex to the exclusion list::
[report]
- exclude_lines =
+ exclude_also =
def __repr__
For example, here's a list of exclusions I've used::
[report]
- exclude_lines =
- pragma: no cover
+ exclude_also =
def __repr__
if self.debug:
if settings.DEBUG
@@ -95,12 +94,14 @@
raise NotImplementedError
if 0:
if __name__ == .__main__.:
+ if TYPE_CHECKING:
class .*\bProtocol\):
@(abc\.)?abstractmethod
-Note that when using the ``exclude_lines`` option in a configuration file, you
-are taking control of the entire list of regexes, so you need to re-specify the
-default "pragma: no cover" match if you still want it to apply.
+The :ref:`config_report_exclude_also` option adds regexes to the built-in
+default list so that you can add your own exclusions. The older
+:ref:`config_report_exclude_lines` option completely overwrites the list of
+regexes.
The regexes only have to match part of a line. Be careful not to over-match. A
value of ``...`` will match any line with more than three characters in it.
diff -Nru python-coverage-6.5.0+dfsg1/doc/faq.rst python-coverage-7.2.7+dfsg1/doc/faq.rst
--- python-coverage-6.5.0+dfsg1/doc/faq.rst 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/faq.rst 2023-05-29 19:46:30.000000000 +0000
@@ -11,6 +11,22 @@
Frequently asked questions
--------------------------
+Q: Why are some of my files not measured?
+.........................................
+
+Coverage.py has a number of mechanisms for deciding which files to measure and
+which to skip. If your files aren't being measured, use the ``--debug=trace``
+:ref:`option `, also settable as ``[run] debug=trace`` in the
+:ref:`settings file `, or as ``COVERAGE_DEBUG=trace`` in an
+environment variable.
+
+This will write a line for each file considered, indicating whether it is
+traced or not, and if not, why not. Be careful though: the output might be
+swallowed by your test runner. If so, a ``COVERAGE_DEBUG_FILE=/tmp/cov.out``
+environment variable can direct the output to a file instead to ensure you see
+everything.
+
+
Q: Why do unexecutable lines show up as executed?
.................................................
@@ -23,11 +39,24 @@
to clean out the old data.
+Q: Why are my function definitions marked as run when I haven't tested them?
+............................................................................
+
+The ``def`` and ``class`` lines in your Python file are executed when the file
+is imported. Those are the lines that define your functions and classes. They
+run even if you never call the functions. It's the body of the functions that
+will be marked as not executed if you don't test them, not the ``def`` lines.
+
+This can mean that your code has a moderate coverage total even if no tests
+have been written or run. This might seem surprising, but it is accurate: the
+``def`` lines have actually been run.
+
+
Q: Why do the bodies of functions show as executed, but the def lines do not?
.............................................................................
-This happens because coverage.py is started after the functions are defined.
-The definition lines are executed without coverage measurement, then
+If this happens, it's because coverage.py has started after the functions are
+defined. The definition lines are executed without coverage measurement, then
coverage.py is started, then the function is called. This means the body is
measured, but the definition of the function itself is not.
@@ -92,7 +121,7 @@
implementations of the trace function. The C implementation runs much faster.
To see what you are running, use ``coverage debug sys``. The output contains
details of the environment, including a line that says either
-``CTrace: available`` or ``CTracer: unavailable``. If it says unavailable,
+``CTracer: available`` or ``CTracer: unavailable``. If it says unavailable,
then you are using the slow Python implementation.
Try re-installing coverage.py to see what happened and if you get the CTracer
@@ -117,9 +146,9 @@
.. _trialcoverage: https://pypi.org/project/trialcoverage/
- - `pytest-coverage`_
+ - `pytest-cov`_
- .. _pytest-coverage: https://pypi.org/project/pytest-coverage/
+ .. _pytest-cov: https://pypi.org/project/pytest-cov/
- `django-coverage`_ for use with Django.
@@ -129,10 +158,11 @@
Q: Where can I get more help with coverage.py?
..............................................
-You can discuss coverage.py or get help using it on the `Testing In Python`_
-mailing list.
+You can discuss coverage.py or get help using it on the `Python discussion
+forums`_. If you ping me (``@nedbat``), there's a higher chance I'll see the
+post.
-.. _Testing In Python: http://lists.idyll.org/listinfo/testing-in-python
+.. _Python discussion forums: https://discuss.python.org/
Bug reports are gladly accepted at the `GitHub issue tracker`_.
@@ -151,6 +181,6 @@
Since 2004, `Ned Batchelder`_ has extended and maintained it with the help of
`many others`_. The :ref:`change history ` has all the details.
-.. _Gareth Rees: http://garethrees.org/
+.. _Gareth Rees: http://garethrees.org/
.. _Ned Batchelder: https://nedbatchelder.com
-.. _many others: https://github.com/nedbat/coveragepy/blob/master/CONTRIBUTORS.txt
+.. _many others: https://github.com/nedbat/coveragepy/blob/master/CONTRIBUTORS.txt
diff -Nru python-coverage-6.5.0+dfsg1/doc/index.rst python-coverage-7.2.7+dfsg1/doc/index.rst
--- python-coverage-6.5.0+dfsg1/doc/index.rst 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/index.rst 2023-05-29 19:46:30.000000000 +0000
@@ -18,18 +18,17 @@
.. PYVERSIONS
-* Python versions 3.7 through 3.11.0 rc2.
-
-* PyPy3 7.3.8.
+* Python versions 3.7 through 3.12.0b1.
+* PyPy3 7.3.11.
.. ifconfig:: prerelease
**This is a pre-release build. The usual warnings about possible bugs
- apply.** The latest stable version is coverage.py 6.4, `described here`_.
-
+ apply.** The latest stable version is coverage.py 6.5.0, `described here`_.
.. _described here: http://coverage.readthedocs.io/
+
For Enterprise
--------------
@@ -57,16 +56,22 @@
#. Install coverage.py::
- $ pip install coverage
+ $ python3 -m pip install coverage
For more details, see :ref:`install`.
#. Use ``coverage run`` to run your test suite and gather data. However you
- normally run your test suite, you can run your test runner under coverage.
- If your test runner command starts with "python", just replace the initial
- "python" with "coverage run".
+ normally run your test suite, you can use your test runner under coverage.
+
+ .. tip::
+ If your test runner command starts with "python", just replace the initial
+ "python" with "coverage run".
+
+ ``python something.py`` becomes ``coverage run something.py``
+
+ ``python -m amodule`` becomes ``coverage run -m amodule``
- Instructions for specific test runners:
+ Other instructions for specific test runners:
- **pytest**
@@ -183,9 +188,10 @@
------------
If the :ref:`FAQ ` doesn't answer your question, you can discuss
-coverage.py or get help using it on the `Testing In Python`_ mailing list.
+coverage.py or get help using it on the `Python discussion forums`_. If you
+ping me (``@nedbat``), there's a higher chance I'll see the post.
-.. _Testing In Python: http://lists.idyll.org/listinfo/testing-in-python
+.. _Python discussion forums: https://discuss.python.org/
Bug reports are gladly accepted at the `GitHub issue tracker`_.
GitHub also hosts the `code repository`_.
@@ -201,7 +207,10 @@
.. _I can be reached: https://nedbatchelder.com/site/aboutned.html
+.. raw:: html
+ For news and other chatter, follow the project on Mastodon:
+ @coveragepy@hachyderm.io.
More information
----------------
@@ -225,4 +234,5 @@
trouble
faq
Change history
+ migrating
sleepy
diff -Nru python-coverage-6.5.0+dfsg1/doc/install.rst python-coverage-7.2.7+dfsg1/doc/install.rst
--- python-coverage-6.5.0+dfsg1/doc/install.rst 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/install.rst 2023-05-29 19:46:30.000000000 +0000
@@ -15,19 +15,19 @@
You can install coverage.py in the usual ways. The simplest way is with pip::
- $ pip install coverage
+ $ python3 -m pip install coverage
.. ifconfig:: prerelease
To install a pre-release version, you will need to specify ``--pre``::
- $ pip install --pre coverage
+ $ python3 -m pip install --pre coverage
or the exact version you want to install:
.. parsed-literal::
- $ pip install |coverage-equals-release|
+ $ python3 -m pip install |coverage-equals-release|
.. _install_extension:
diff -Nru python-coverage-6.5.0+dfsg1/doc/migrating.rst python-coverage-7.2.7+dfsg1/doc/migrating.rst
--- python-coverage-6.5.0+dfsg1/doc/migrating.rst 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/migrating.rst 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +1,54 @@
+.. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+.. _migrating:
+
+==========================
+Migrating between versions
+==========================
+
+New versions of coverage.py or Python might require you to adjust your
+settings, options, or other aspects how you use coverage.py. This page details
+those changes.
+
+.. _migrating_cov7:
+
+Migrating to coverage.py 7.x
+----------------------------
+
+Consider these changes when migrating to coverage.py 7.x:
+
+- The way that wildcards when specifying file paths work in certain cases has
+ changed in 7.x:
+
+ - Previously, ``*`` would incorrectly match directory separators, making
+ precise matching difficult. Patterns such as ``*tests/*``
+ will need to be changed to ``*/tests/*``.
+
+ - ``**`` now matches any number of nested directories. If you wish to retain
+ the behavior of ``**/tests/*`` in previous versions then ``*/**/tests/*``
+ can be used instead.
+
+- When remapping file paths with ``[paths]``, a path will be remapped only if
+ the resulting path exists. Ensure that remapped ``[paths]`` exist when
+ upgrading as this is now being enforced.
+
+- The :ref:`config_report_exclude_also` setting is new in 7.2.0. It adds
+ exclusion regexes while keeping the default built-in set. It's better than
+ the older :ref:`config_report_exclude_lines` setting, which overwrote the
+ entire list. Newer versions of coverage.py will be adding to the default set
+ of exclusions. Using ``exclude_also`` will let you benefit from those
+ updates.
+
+
+.. _migrating_py312:
+
+Migrating to Python 3.12
+------------------------
+
+Keep these things in mind when running under Python 3.12:
+
+- Python 3.12 now inlines list, dict, and set comprehensions. Previously, they
+ were compiled as functions that were called internally. Coverage.py would
+ warn you if comprehensions weren't fully completed, but this no longer
+ happens with Python 3.12.
diff -Nru python-coverage-6.5.0+dfsg1/doc/plugins.rst python-coverage-7.2.7+dfsg1/doc/plugins.rst
--- python-coverage-6.5.0+dfsg1/doc/plugins.rst 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/plugins.rst 2023-05-29 19:46:30.000000000 +0000
@@ -29,7 +29,7 @@
.. code-block:: sh
- $ pip install something
+ $ python3 -m pip install something
#. Configure coverage.py to use the plug-in. You do this by editing (or
creating) your .coveragerc file, as described in :ref:`config`. The
diff -Nru python-coverage-6.5.0+dfsg1/doc/python-coverage.1.txt python-coverage-7.2.7+dfsg1/doc/python-coverage.1.txt
--- python-coverage-6.5.0+dfsg1/doc/python-coverage.1.txt 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/python-coverage.1.txt 2023-05-29 19:46:30.000000000 +0000
@@ -8,7 +8,7 @@
:Author: Ned Batchelder
:Author: |author|
-:Date: 2022-01-25
+:Date: 2022-12-03
:Copyright: Apache 2.0 license, attribution and disclaimer required.
:Manual section: 1
:Manual group: Coverage.py
@@ -299,6 +299,9 @@
\--fail-under `MIN`
Exit with a status of 2 if the total coverage is less than `MIN`.
+ \--format `FORMAT`
+ Output format, either text (default), markdown, or total.
+
\-i, --ignore-errors
Ignore errors while reading source files.
diff -Nru python-coverage-6.5.0+dfsg1/doc/requirements.in python-coverage-7.2.7+dfsg1/doc/requirements.in
--- python-coverage-6.5.0+dfsg1/doc/requirements.in 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/requirements.in 2023-05-29 19:46:30.000000000 +0000
@@ -9,6 +9,7 @@
cogapp
#doc8
pyenchant
+scriv # for writing GitHub releases
sphinx
sphinx-autobuild
sphinx_rtd_theme
diff -Nru python-coverage-6.5.0+dfsg1/doc/requirements.pip python-coverage-7.2.7+dfsg1/doc/requirements.pip
--- python-coverage-6.5.0+dfsg1/doc/requirements.pip 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/requirements.pip 2023-05-29 19:46:30.000000000 +0000
@@ -1,211 +1,106 @@
#
-# This file is autogenerated by pip-compile with python 3.7
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.7
+# by the following command:
#
# make upgrade
#
-alabaster==0.7.12 \
- --hash=sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359 \
- --hash=sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02
- # via sphinx
-babel==2.10.3 \
- --hash=sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51 \
- --hash=sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb
- # via sphinx
-certifi==2022.5.18.1 \
- --hash=sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7 \
- --hash=sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a
- # via
- # -c doc/../requirements/pins.pip
- # requests
-charset-normalizer==2.1.1 \
- --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \
- --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f
+alabaster==0.7.13
+ # via sphinx
+attrs==23.1.0
+ # via scriv
+babel==2.12.1
+ # via sphinx
+certifi==2023.5.7
# via requests
-cogapp==3.3.0 \
- --hash=sha256:1be95183f70282422d594fa42426be6923070a4bd8335621f6347f3aeee81db0 \
- --hash=sha256:8b5b5f6063d8ee231961c05da010cb27c30876b2279e23ad0eae5f8f09460d50
- # via -r doc/requirements.in
-colorama==0.4.5 \
- --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \
- --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4
+charset-normalizer==3.1.0
+ # via requests
+click==8.1.3
+ # via
+ # click-log
+ # scriv
+click-log==0.4.0
+ # via scriv
+cogapp==3.3.0
+ # via -r doc/requirements.in
+colorama==0.4.6
# via sphinx-autobuild
-docutils==0.17.1 \
- --hash=sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125 \
- --hash=sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61
+docutils==0.18.1
# via
# sphinx
# sphinx-rtd-theme
-idna==3.4 \
- --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
- --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+idna==3.4
# via requests
-imagesize==1.4.1 \
- --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \
- --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a
- # via sphinx
-importlib-metadata==4.12.0 \
- --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \
- --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23
+imagesize==1.4.1
+ # via sphinx
+importlib-metadata==6.6.0
# via
+ # attrs
+ # click
# sphinx
# sphinxcontrib-spelling
-jinja2==3.1.2 \
- --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
- --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
- # via sphinx
-livereload==2.6.3 \
- --hash=sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869
+jinja2==3.1.2
+ # via
+ # scriv
+ # sphinx
+livereload==2.6.3
# via sphinx-autobuild
-markupsafe==2.1.1 \
- --hash=sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003 \
- --hash=sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88 \
- --hash=sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5 \
- --hash=sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7 \
- --hash=sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a \
- --hash=sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603 \
- --hash=sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1 \
- --hash=sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135 \
- --hash=sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247 \
- --hash=sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6 \
- --hash=sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601 \
- --hash=sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77 \
- --hash=sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02 \
- --hash=sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e \
- --hash=sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63 \
- --hash=sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f \
- --hash=sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980 \
- --hash=sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b \
- --hash=sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812 \
- --hash=sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff \
- --hash=sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96 \
- --hash=sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1 \
- --hash=sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925 \
- --hash=sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a \
- --hash=sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6 \
- --hash=sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e \
- --hash=sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f \
- --hash=sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4 \
- --hash=sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f \
- --hash=sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3 \
- --hash=sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c \
- --hash=sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a \
- --hash=sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417 \
- --hash=sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a \
- --hash=sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a \
- --hash=sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37 \
- --hash=sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452 \
- --hash=sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933 \
- --hash=sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a \
- --hash=sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7
+markupsafe==2.1.2
# via jinja2
-packaging==21.3 \
- --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
- --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
- # via sphinx
-pyenchant==3.2.2 \
- --hash=sha256:1cf830c6614362a78aab78d50eaf7c6c93831369c52e1bb64ffae1df0341e637 \
- --hash=sha256:5a636832987eaf26efe971968f4d1b78e81f62bca2bde0a9da210c7de43c3bce \
- --hash=sha256:5facc821ece957208a81423af7d6ec7810dad29697cb0d77aae81e4e11c8e5a6 \
- --hash=sha256:6153f521852e23a5add923dbacfbf4bebbb8d70c4e4bad609a8e0f9faeb915d1
+packaging==23.1
+ # via sphinx
+pyenchant==3.2.2
# via
# -r doc/requirements.in
# sphinxcontrib-spelling
-pygments==2.13.0 \
- --hash=sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1 \
- --hash=sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42
- # via sphinx
-pyparsing==3.0.9 \
- --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
- --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
- # via packaging
-pytz==2022.2.1 \
- --hash=sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197 \
- --hash=sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5
+pygments==2.15.1
+ # via sphinx
+pytz==2023.3
# via babel
-requests==2.28.1 \
- --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \
- --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349
- # via sphinx
-six==1.16.0 \
- --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
- --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+requests==2.31.0
+ # via
+ # scriv
+ # sphinx
+scriv==1.3.1
+ # via -r doc/requirements.in
+six==1.16.0
# via livereload
-snowballstemmer==2.2.0 \
- --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
- --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
- # via sphinx
-sphinx==5.2.1 \
- --hash=sha256:3dcf00fcf82cf91118db9b7177edea4fc01998976f893928d0ab0c58c54be2ca \
- --hash=sha256:c009bb2e9ac5db487bcf53f015504005a330ff7c631bb6ab2604e0d65bae8b54
+snowballstemmer==2.2.0
+ # via sphinx
+sphinx==5.3.0
# via
# -r doc/requirements.in
# sphinx-autobuild
# sphinx-rtd-theme
+ # sphinxcontrib-jquery
# sphinxcontrib-restbuilder
# sphinxcontrib-spelling
-sphinx-autobuild==2021.3.14 \
- --hash=sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac \
- --hash=sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05
- # via -r doc/requirements.in
-sphinx-rtd-theme==1.0.0 \
- --hash=sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8 \
- --hash=sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c
- # via -r doc/requirements.in
-sphinxcontrib-applehelp==1.0.2 \
- --hash=sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a \
- --hash=sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58
- # via sphinx
-sphinxcontrib-devhelp==1.0.2 \
- --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \
- --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4
- # via sphinx
-sphinxcontrib-htmlhelp==2.0.0 \
- --hash=sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07 \
- --hash=sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2
- # via sphinx
-sphinxcontrib-jsmath==1.0.1 \
- --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \
- --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8
- # via sphinx
-sphinxcontrib-qthelp==1.0.3 \
- --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \
- --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6
- # via sphinx
-sphinxcontrib-restbuilder==0.3 \
- --hash=sha256:6b3ee9394b5ec5e73e6afb34d223530d0b9098cb7562f9c5e364e6d6b41410ce \
- --hash=sha256:6ba2ddc7a87d845c075c1b2e00d541bd1c8400488e50e32c9b4169ccdd9f30cb
- # via -r doc/requirements.in
-sphinxcontrib-serializinghtml==1.1.5 \
- --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \
- --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952
- # via sphinx
-sphinxcontrib-spelling==7.6.0 \
- --hash=sha256:292cd7e1f73a763451693b4d48c9bded151084f6a91e5337733e9fa8715d20ec \
- --hash=sha256:6c1313618412511109f7b76029fbd60df5aa4acf67a2dc9cd1b1016d15e882ff
- # via -r doc/requirements.in
-tornado==6.2 \
- --hash=sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca \
- --hash=sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72 \
- --hash=sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23 \
- --hash=sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8 \
- --hash=sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b \
- --hash=sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9 \
- --hash=sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13 \
- --hash=sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75 \
- --hash=sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac \
- --hash=sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e \
- --hash=sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b
+sphinx-autobuild==2021.3.14
+ # via -r doc/requirements.in
+sphinx-rtd-theme==1.2.1
+ # via -r doc/requirements.in
+sphinxcontrib-applehelp==1.0.2
+ # via sphinx
+sphinxcontrib-devhelp==1.0.2
+ # via sphinx
+sphinxcontrib-htmlhelp==2.0.0
+ # via sphinx
+sphinxcontrib-jquery==4.1
+ # via sphinx-rtd-theme
+sphinxcontrib-jsmath==1.0.1
+ # via sphinx
+sphinxcontrib-qthelp==1.0.3
+ # via sphinx
+sphinxcontrib-restbuilder==0.3
+ # via -r doc/requirements.in
+sphinxcontrib-serializinghtml==1.1.5
+ # via sphinx
+sphinxcontrib-spelling==8.0.0
+ # via -r doc/requirements.in
+tornado==6.2
# via livereload
-typing-extensions==4.3.0 \
- --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \
- --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6
+typing-extensions==4.6.2
# via importlib-metadata
-urllib3==1.26.12 \
- --hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \
- --hash=sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997
+urllib3==2.0.2
# via requests
-zipp==3.8.1 \
- --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \
- --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009
+zipp==3.15.0
# via importlib-metadata
diff -Nru python-coverage-6.5.0+dfsg1/doc/source.rst python-coverage-7.2.7+dfsg1/doc/source.rst
--- python-coverage-6.5.0+dfsg1/doc/source.rst 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/source.rst 2023-05-29 19:46:30.000000000 +0000
@@ -32,7 +32,7 @@
If the source option is specified, only code in those locations will be
measured. Specifying the source option also enables coverage.py to report on
-unexecuted files, since it can search the source tree for files that haven't
+un-executed files, since it can search the source tree for files that haven't
been measured at all. Only importable files (ones at the root of the tree, or
in directories with a ``__init__.py`` file) will be considered. Files with
unusual punctuation in their names will be skipped (they are assumed to be
@@ -59,10 +59,10 @@
.. highlight:: ini
-The ``include`` and ``omit`` file name patterns follow typical shell syntax:
-``*`` matches any number of characters and ``?`` matches a single character.
-Patterns that start with a wildcard character are used as-is, other patterns
-are interpreted relative to the current directory::
+The ``include`` and ``omit`` file name patterns follow common shell syntax,
+described below in :ref:`source_glob`. Patterns that start with a wildcard
+character are used as-is, other patterns are interpreted relative to the
+current directory::
[run]
omit =
@@ -77,7 +77,7 @@
the source that will be measured.
If both ``source`` and ``include`` are set, the ``include`` value is ignored
-and a warning is printed on the standard output.
+and a warning is issued.
.. _source_reporting:
@@ -103,3 +103,22 @@
Note that these are ways of specifying files to measure. You can also exclude
individual source lines. See :ref:`excluding` for details.
+
+
+.. _source_glob:
+
+File patterns
+-------------
+
+File path patterns are used for include and omit, and for combining path
+remapping. They follow common shell syntax:
+
+- ``*`` matches any number of file name characters, not including the directory
+ separator.
+
+- ``?`` matches a single file name character.
+
+- ``**`` matches any number of nested directory names, including none.
+
+- Both ``/`` and ``\`` will match either a slash or a backslash, to make
+ cross-platform matching easier.
diff -Nru python-coverage-6.5.0+dfsg1/doc/trouble.rst python-coverage-7.2.7+dfsg1/doc/trouble.rst
--- python-coverage-6.5.0+dfsg1/doc/trouble.rst 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/doc/trouble.rst 2023-05-29 19:46:30.000000000 +0000
@@ -25,8 +25,8 @@
Things that don't work
----------------------
-There are a number of popular modules, packages, and libraries that prevent
-coverage.py from working properly:
+There are a few modules or functions that prevent coverage.py from working
+properly:
* `execv`_, or one of its variants. These end the current program and replace
it with a new one. This doesn't save the collected coverage data, so your
diff -Nru python-coverage-6.5.0+dfsg1/.editorconfig python-coverage-7.2.7+dfsg1/.editorconfig
--- python-coverage-6.5.0+dfsg1/.editorconfig 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/.editorconfig 2023-05-29 19:46:30.000000000 +0000
@@ -18,6 +18,9 @@
[*.py]
max_line_length = 100
+[*.pyi]
+max_line_length = 100
+
[*.c]
max_line_length = 100
@@ -30,6 +33,12 @@
[*.rst]
max_line_length = 79
+[*.tok]
+trim_trailing_whitespace = false
+
+[*_dos.tok]
+end_of_line = crlf
+
[Makefile]
indent_style = tab
indent_size = 8
diff -Nru python-coverage-6.5.0+dfsg1/.git-blame-ignore-revs python-coverage-7.2.7+dfsg1/.git-blame-ignore-revs
--- python-coverage-6.5.0+dfsg1/.git-blame-ignore-revs 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/.git-blame-ignore-revs 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +1,14 @@
+# Commits to ignore when doing git-blame.
+
+# 2023-01-05 style: use good style for annotated defaults parameters
+78444f4c06df6a634fa67dd99ee7c07b6b633d9e
+
+# 2023-01-06 style(perf): blacken lab/benchmark.py
+bf6c12f5da54db7c5c0cc47cbf22c70f686e8236
+
+# 2023-03-22 style: use double-quotes
+16abd82b6e87753184e8308c4b2606ff3979f8d3
+b7be64538aa480fce641349d3053e9a84862d571
+
+# 2023-04-01 style: use double-quotes in JavaScript
+b03ab92bae24c54f1d5a98baa3af6b9a18de4d36
diff -Nru python-coverage-6.5.0+dfsg1/.github/ISSUE_TEMPLATE/bug_report.md python-coverage-7.2.7+dfsg1/.github/ISSUE_TEMPLATE/bug_report.md
--- python-coverage-6.5.0+dfsg1/.github/ISSUE_TEMPLATE/bug_report.md 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/.github/ISSUE_TEMPLATE/bug_report.md 2023-05-29 19:46:30.000000000 +0000
@@ -16,7 +16,7 @@
1. What version of coverage.py shows the problem? The output of `coverage debug sys` is helpful.
1. What versions of what packages do you have installed? The output of `pip freeze` is helpful.
1. What code shows the problem? Give us a specific commit of a specific repo that we can check out. If you've already worked around the problem, please provide a commit before that fix.
-1. What commands did you run?
+1. What commands should we run to reproduce the problem? *Be specific*. Include everything, even `git clone`, `pip install`, and so on. Explain like we're five!
**Expected behavior**
A clear and concise description of what you expected to happen.
diff -Nru python-coverage-6.5.0+dfsg1/.github/ISSUE_TEMPLATE/config.yml python-coverage-7.2.7+dfsg1/.github/ISSUE_TEMPLATE/config.yml
--- python-coverage-6.5.0+dfsg1/.github/ISSUE_TEMPLATE/config.yml 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/.github/ISSUE_TEMPLATE/config.yml 2023-05-29 19:46:30.000000000 +0000
@@ -8,9 +8,6 @@
- name: Frequently Asked Questions
url: https://coverage.readthedocs.io/en/latest/faq.html
about: Some common problems are described here.
- - name: Testing in Python mailing list
- url: http://lists.idyll.org/listinfo/testing-in-python
- about: Ask questions about using coverage.py here.
- name: Tidelift security contact
url: https://tidelift.com/security
about: Please report security vulnerabilities here.
diff -Nru python-coverage-6.5.0+dfsg1/.github/workflows/coverage.yml python-coverage-7.2.7+dfsg1/.github/workflows/coverage.yml
--- python-coverage-6.5.0+dfsg1/.github/workflows/coverage.yml 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/.github/workflows/coverage.yml 2023-05-29 19:46:30.000000000 +0000
@@ -18,6 +18,7 @@
env:
PIP_DISABLE_PIP_VERSION_CHECK: 1
+ FORCE_COLOR: 1 # Get colored pytest output
permissions:
contents: read
@@ -28,17 +29,17 @@
jobs:
coverage:
- name: "Python ${{ matrix.python-version }} on ${{ matrix.os }}"
- runs-on: "${{ matrix.os }}"
+ name: "${{ matrix.python-version }} on ${{ matrix.os }}"
+ runs-on: "${{ matrix.os }}-latest"
strategy:
matrix:
os:
- - ubuntu-latest
- - macos-latest
- - windows-latest
+ - ubuntu
+ - macos
+ - windows
python-version:
- # When changing this list, be sure to check the [gh-actions] list in
+ # When changing this list, be sure to check the [gh] list in
# tox.ini so that tox will run properly. PYVERSIONS
# Available versions:
# https://github.com/actions/python-versions/blob/main/versions-manifest.json
@@ -46,15 +47,26 @@
- "3.8"
- "3.9"
- "3.10"
- - "3.11.0-rc.2"
+ - "3.11"
+ - "3.12"
- "pypy-3.7"
+ - "pypy-3.8"
+ - "pypy-3.9"
exclude:
# Windows PyPy doesn't seem to work?
- os: windows-latest
python-version: "pypy-3.7"
+ - os: windows-latest
+ python-version: "pypy-3.8"
+ - os: windows-latest
+ python-version: "pypy-3.9"
# Mac PyPy always takes the longest, and doesn't add anything.
- os: macos-latest
python-version: "pypy-3.7"
+ - os: macos-latest
+ python-version: "pypy-3.8"
+ - os: macos-latest
+ python-version: "pypy-3.9"
# If one job fails, stop the whole thing.
fail-fast: true
@@ -66,6 +78,7 @@
uses: "actions/setup-python@v4"
with:
python-version: "${{ matrix.python-version }}"
+ allow-prereleases: true
cache: pip
cache-dependency-path: 'requirements/*.pip'
@@ -74,7 +87,7 @@
set -xe
python -VV
python -m site
- python -m pip install --require-hashes -r requirements/tox.pip
+ python -m pip install -r requirements/tox.pip
- name: "Run tox coverage for ${{ matrix.python-version }}"
env:
@@ -84,6 +97,13 @@
set -xe
python -m tox
+ - name: "Combine data"
+ env:
+ COVERAGE_RCFILE: "metacov.ini"
+ run: |
+ python -m coverage combine
+ mv .metacov .metacov.${{ matrix.python-version }}.${{ matrix.os }}
+
- name: "Upload coverage data"
uses: actions/upload-artifact@v3
with:
@@ -94,6 +114,10 @@
name: "Combine coverage data"
needs: coverage
runs-on: ubuntu-latest
+ outputs:
+ total: ${{ steps.total.outputs.total }}
+ env:
+ COVERAGE_RCFILE: "metacov.ini"
steps:
- name: "Check out the repo"
@@ -102,7 +126,7 @@
- name: "Set up Python"
uses: "actions/setup-python@v4"
with:
- python-version: "3.8"
+ python-version: "3.7" # Minimum of PYVERSIONS
cache: pip
cache-dependency-path: 'requirements/*.pip'
@@ -122,13 +146,10 @@
- name: "Combine and report"
id: combine
env:
- COVERAGE_RCFILE: "metacov.ini"
- COVERAGE_METAFILE: ".metacov"
COVERAGE_CONTEXT: "yes"
run: |
set -xe
- python -m igor combine_html
- python -m coverage json
+ python igor.py combine_html
- name: "Upload HTML report"
uses: actions/upload-artifact@v3
@@ -136,11 +157,10 @@
name: html_report
path: htmlcov
- - name: "Upload JSON report"
- uses: actions/upload-artifact@v3
- with:
- name: json_report
- path: coverage.json
+ - name: "Get total"
+ id: total
+ run: |
+ echo "total=$(python -m coverage report --format=total)" >> $GITHUB_OUTPUT
publish:
name: "Publish coverage report"
@@ -148,45 +168,46 @@
runs-on: ubuntu-latest
steps:
- - name: "Checkout reports repo"
- run: |
- set -xe
- git clone --depth=1 --no-checkout https://${{ secrets.COVERAGE_REPORTS_TOKEN }}@github.com/nedbat/coverage-reports reports_repo
- cd reports_repo
- git sparse-checkout init --cone
- git sparse-checkout set --skip-checks '/*' '!/reports'
- git config user.name nedbat
- git config user.email ned@nedbatchelder.com
- git checkout main
-
- - name: "Download coverage JSON report"
- uses: actions/download-artifact@v3
- with:
- name: json_report
-
- name: "Compute info for later steps"
id: info
run: |
set -xe
- export TOTAL=$(python -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])")
export SHA10=$(echo ${{ github.sha }} | cut -c 1-10)
export SLUG=$(date +'%Y%m%d')_$SHA10
export REPORT_DIR=reports/$SLUG/htmlcov
export REF="${{ github.ref }}"
- echo "total=$TOTAL" >> $GITHUB_ENV
+ echo "total=${{ needs.combine.outputs.total }}" >> $GITHUB_ENV
echo "sha10=$SHA10" >> $GITHUB_ENV
echo "slug=$SLUG" >> $GITHUB_ENV
echo "report_dir=$REPORT_DIR" >> $GITHUB_ENV
- echo "url=https://nedbat.github.io/coverage-reports/$REPORT_DIR" >> $GITHUB_ENV
+ echo "url=https://htmlpreview.github.io/?https://github.com/nedbat/coverage-reports/blob/main/reports/$SLUG/htmlcov/index.html" >> $GITHUB_ENV
echo "branch=${REF#refs/heads/}" >> $GITHUB_ENV
+ - name: "Summarize"
+ run: |
+ echo '### Total coverage: ${{ env.total }}%' >> $GITHUB_STEP_SUMMARY
+
+ - name: "Checkout reports repo"
+ if: ${{ github.ref == 'refs/heads/master' }}
+ run: |
+ set -xe
+ git clone --depth=1 --no-checkout https://${{ secrets.COVERAGE_REPORTS_TOKEN }}@github.com/nedbat/coverage-reports reports_repo
+ cd reports_repo
+ git sparse-checkout init --cone
+ git sparse-checkout set --skip-checks '/*' '!/reports'
+ git config user.name nedbat
+ git config user.email ned@nedbatchelder.com
+ git checkout main
+
- name: "Download coverage HTML report"
+ if: ${{ github.ref == 'refs/heads/master' }}
uses: actions/download-artifact@v3
with:
name: html_report
path: reports_repo/${{ env.report_dir }}
- name: "Push to report repo"
+ if: ${{ github.ref == 'refs/heads/master' }}
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
@@ -198,6 +219,8 @@
# Make the commit message.
echo "${{ env.total }}% - $COMMIT_MESSAGE" > commit.txt
echo "" >> commit.txt
+ echo "[View the report](${{ env.url }})" >> commit.txt
+ echo "" >> commit.txt
echo "${{ env.url }}" >> commit.txt
echo "${{ env.sha10 }}: ${{ env.branch }}" >> commit.txt
# Commit.
@@ -207,11 +230,12 @@
git add ${{ env.report_dir }} latest.html
git commit --file=../commit.txt
git push
+ echo '[${{ env.url }}](${{ env.url }})' >> $GITHUB_STEP_SUMMARY
- name: "Create badge"
+ if: ${{ github.ref == 'refs/heads/master' }}
# https://gist.githubusercontent.com/nedbat/8c6980f77988a327348f9b02bbaf67f5
- # uses: schneegans/dynamic-badges-action@v1.4.0
- uses: schneegans/dynamic-badges-action@54d929a33e7521ab6bf19d323d28fb7b876c53f7
+ uses: schneegans/dynamic-badges-action@5d424ad4060f866e4d1dab8f8da0456e6b1c4f56
with:
auth: ${{ secrets.METACOV_GIST_SECRET }}
gistID: 8c6980f77988a327348f9b02bbaf67f5
@@ -221,8 +245,3 @@
minColorRange: 60
maxColorRange: 95
valColorRange: ${{ env.total }}
-
- - name: "Create summary"
- run: |
- echo '### Total coverage: ${{ env.total }}%' >> $GITHUB_STEP_SUMMARY
- echo '[${{ env.url }}](${{ env.url }})' >> $GITHUB_STEP_SUMMARY
diff -Nru python-coverage-6.5.0+dfsg1/.github/workflows/dependency-review.yml python-coverage-7.2.7+dfsg1/.github/workflows/dependency-review.yml
--- python-coverage-6.5.0+dfsg1/.github/workflows/dependency-review.yml 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/.github/workflows/dependency-review.yml 2023-05-29 19:46:30.000000000 +0000
@@ -4,8 +4,15 @@
#
# Source repository: https://github.com/actions/dependency-review-action
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
+
name: 'Dependency Review'
-on: [pull_request]
+on:
+ push:
+ branches:
+ - master
+ - nedbat/*
+ pull_request:
+ workflow_dispatch:
permissions:
contents: read
@@ -17,4 +24,7 @@
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: 'Dependency Review'
- uses: actions/dependency-review-action@v2
+ uses: actions/dependency-review-action@v3
+ with:
+ base-ref: ${{ github.event.pull_request.base.sha || 'master' }}
+ head-ref: ${{ github.event.pull_request.head.sha || github.ref }}
diff -Nru python-coverage-6.5.0+dfsg1/.github/workflows/kit.yml python-coverage-7.2.7+dfsg1/.github/workflows/kit.yml
--- python-coverage-6.5.0+dfsg1/.github/workflows/kit.yml 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/.github/workflows/kit.yml 2023-05-29 19:46:30.000000000 +0000
@@ -47,7 +47,7 @@
jobs:
wheels:
- name: "Build ${{ matrix.os }} ${{ matrix.py }} ${{ matrix.arch }} wheels"
+ name: "${{ matrix.py }} ${{ matrix.os }} ${{ matrix.arch }} wheels"
runs-on: ${{ matrix.os }}-latest
strategy:
matrix:
@@ -77,11 +77,12 @@
# }
# # PYVERSIONS. Available versions:
# # https://github.com/actions/python-versions/blob/main/versions-manifest.json
- # pys = ["cp37", "cp38", "cp39", "cp310", "cp311"]
+ # # PyPy versions are handled further below in the "pypy" step.
+ # pys = ["cp37", "cp38", "cp39", "cp310", "cp311", "cp312"]
#
# # Some OS/arch combinations need overrides for the Python versions:
# os_arch_pys = {
- # ("macos", "arm64"): ["cp38", "cp39", "cp310"],
+ # ("macos", "arm64"): ["cp38", "cp39", "cp310", "cp311"],
# }
#
# #----- ^^^ ---------------------- ^^^ -----
@@ -102,42 +103,48 @@
- {"os": "ubuntu", "py": "cp39", "arch": "x86_64"}
- {"os": "ubuntu", "py": "cp310", "arch": "x86_64"}
- {"os": "ubuntu", "py": "cp311", "arch": "x86_64"}
+ - {"os": "ubuntu", "py": "cp312", "arch": "x86_64"}
- {"os": "ubuntu", "py": "cp37", "arch": "i686"}
- {"os": "ubuntu", "py": "cp38", "arch": "i686"}
- {"os": "ubuntu", "py": "cp39", "arch": "i686"}
- {"os": "ubuntu", "py": "cp310", "arch": "i686"}
- {"os": "ubuntu", "py": "cp311", "arch": "i686"}
+ - {"os": "ubuntu", "py": "cp312", "arch": "i686"}
- {"os": "ubuntu", "py": "cp37", "arch": "aarch64"}
- {"os": "ubuntu", "py": "cp38", "arch": "aarch64"}
- {"os": "ubuntu", "py": "cp39", "arch": "aarch64"}
- {"os": "ubuntu", "py": "cp310", "arch": "aarch64"}
- {"os": "ubuntu", "py": "cp311", "arch": "aarch64"}
+ - {"os": "ubuntu", "py": "cp312", "arch": "aarch64"}
- {"os": "macos", "py": "cp38", "arch": "arm64"}
- {"os": "macos", "py": "cp39", "arch": "arm64"}
- {"os": "macos", "py": "cp310", "arch": "arm64"}
+ - {"os": "macos", "py": "cp311", "arch": "arm64"}
- {"os": "macos", "py": "cp37", "arch": "x86_64"}
- {"os": "macos", "py": "cp38", "arch": "x86_64"}
- {"os": "macos", "py": "cp39", "arch": "x86_64"}
- {"os": "macos", "py": "cp310", "arch": "x86_64"}
- {"os": "macos", "py": "cp311", "arch": "x86_64"}
+ - {"os": "macos", "py": "cp312", "arch": "x86_64"}
- {"os": "windows", "py": "cp37", "arch": "x86"}
- {"os": "windows", "py": "cp38", "arch": "x86"}
- {"os": "windows", "py": "cp39", "arch": "x86"}
- {"os": "windows", "py": "cp310", "arch": "x86"}
- {"os": "windows", "py": "cp311", "arch": "x86"}
+ - {"os": "windows", "py": "cp312", "arch": "x86"}
- {"os": "windows", "py": "cp37", "arch": "AMD64"}
- {"os": "windows", "py": "cp38", "arch": "AMD64"}
- {"os": "windows", "py": "cp39", "arch": "AMD64"}
- {"os": "windows", "py": "cp310", "arch": "AMD64"}
- {"os": "windows", "py": "cp311", "arch": "AMD64"}
- # [[[end]]] (checksum: 428e5138336453464dde968cc3149f4f)
+ - {"os": "windows", "py": "cp312", "arch": "AMD64"}
+ # [[[end]]] (checksum: 5e62f362263935c1e3a21299f8a1b649)
fail-fast: false
steps:
- name: "Setup QEMU"
if: matrix.os == 'ubuntu'
- # uses: docker/setup-qemu-action@v2
- uses: docker/setup-qemu-action@8b122486cedac8393e77aa9734c3528886e4a1a8
+ uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18
with:
platforms: arm64
@@ -147,19 +154,21 @@
- name: "Install Python 3.8"
uses: actions/setup-python@v4
with:
+ # PYVERSIONS
python-version: "3.8"
cache: pip
cache-dependency-path: 'requirements/*.pip'
- name: "Install tools"
run: |
- python -m pip install --require-hashes -r requirements/kit.pip
+ python -m pip install -r requirements/kit.pip
- name: "Build wheels"
env:
CIBW_BUILD: ${{ matrix.py }}-*
CIBW_ARCHS: ${{ matrix.arch }}
CIBW_ENVIRONMENT: PIP_DISABLE_PIP_VERSION_CHECK=1
+ CIBW_PRERELEASE_PYTHONS: True
CIBW_TEST_COMMAND: python -c "from coverage.tracer import CTracer; print('CTracer OK!')"
run: |
python -m cibuildwheel --output-dir wheelhouse
@@ -173,9 +182,10 @@
with:
name: dist
path: wheelhouse/*.whl
+ retention-days: 7
sdist:
- name: "Build source distribution"
+ name: "Source distribution"
runs-on: ubuntu-latest
steps:
- name: "Check out the repo"
@@ -184,13 +194,14 @@
- name: "Install Python 3.8"
uses: actions/setup-python@v4
with:
+ # PYVERSIONS
python-version: "3.8"
cache: pip
cache-dependency-path: 'requirements/*.pip'
- name: "Install tools"
run: |
- python -m pip install --require-hashes -r requirements/kit.pip
+ python -m pip install -r requirements/kit.pip
- name: "Build sdist"
run: |
@@ -205,9 +216,10 @@
with:
name: dist
path: dist/*.tar.gz
+ retention-days: 7
pypy:
- name: "Build PyPy wheel"
+ name: "PyPy wheel"
runs-on: ubuntu-latest
steps:
- name: "Check out the repo"
@@ -216,8 +228,7 @@
- name: "Install PyPy"
uses: actions/setup-python@v4
with:
- # PYVERSIONS
- python-version: "pypy-3.7"
+ python-version: "pypy-3.7" # Minimum of PyPy PYVERSIONS
cache: pip
cache-dependency-path: 'requirements/*.pip'
@@ -227,9 +238,9 @@
- name: "Build wheel"
run: |
- # One wheel works for all PyPy versions.
+ # One wheel works for all PyPy versions. PYVERSIONS
# yes, this is weird syntax: https://github.com/pypa/build/issues/202
- pypy3 -m build -w -C="--global-option=--python-tag" -C="--global-option=pp36.pp37.pp38"
+ pypy3 -m build -w -C="--global-option=--python-tag" -C="--global-option=pp37.pp38.pp39"
- name: "List wheels"
run: |
@@ -240,3 +251,40 @@
with:
name: dist
path: dist/*.whl
+ retention-days: 7
+
+ sign:
+ # This signs our artifacts, but we don't use the signatures for anything
+ # yet. Someday maybe PyPI will have a way to upload and verify them.
+ name: "Sign artifacts"
+ needs:
+ - wheels
+ - sdist
+ - pypy
+ runs-on: ubuntu-latest
+ permissions:
+ id-token: write
+ steps:
+ - name: "Download artifacts"
+ uses: actions/download-artifact@v3
+ with:
+ name: dist
+
+ - name: "Sign artifacts"
+ uses: sigstore/gh-action-sigstore-python@v1.2.3
+ with:
+ inputs: coverage-*.*
+
+ - name: "List files"
+ run: |
+ ls -alR
+
+ - name: "Upload signatures"
+ uses: actions/upload-artifact@v3
+ with:
+ name: signatures
+ path: |
+ *.crt
+ *.sig
+ *.sigstore
+ retention-days: 7
diff -Nru python-coverage-6.5.0+dfsg1/.github/workflows/python-nightly.yml python-coverage-7.2.7+dfsg1/.github/workflows/python-nightly.yml
--- python-coverage-6.5.0+dfsg1/.github/workflows/python-nightly.yml 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/.github/workflows/python-nightly.yml 2023-05-29 19:46:30.000000000 +0000
@@ -31,23 +31,29 @@
jobs:
tests:
- name: "Python ${{ matrix.python-version }}"
- runs-on: ubuntu-latest
+ name: "${{ matrix.python-version }}"
+ # Choose a recent Ubuntu that deadsnakes still builds all the versions for.
+ # For example, deadsnakes doesn't provide 3.10 nightly for 22.04 (jammy)
+ # because jammy ships 3.10, and deadsnakes doesn't want to clobber it.
+ # https://launchpad.net/~deadsnakes/+archive/ubuntu/nightly/+packages
+ # https://github.com/deadsnakes/issues/issues/234
+ runs-on: ubuntu-20.04
strategy:
matrix:
python-version:
- # When changing this list, be sure to check the [gh-actions] list in
+ # When changing this list, be sure to check the [gh] list in
# tox.ini so that tox will run properly. PYVERSIONS
# Available versions:
# https://launchpad.net/~deadsnakes/+archive/ubuntu/nightly/+packages
- - "3.9-dev"
- "3.10-dev"
- "3.11-dev"
+ - "3.12-dev"
# https://github.com/actions/setup-python#available-versions-of-pypy
- "pypy-3.7-nightly"
- "pypy-3.8-nightly"
- "pypy-3.9-nightly"
+ - "pypy-3.10-nightly"
fail-fast: false
steps:
@@ -55,8 +61,7 @@
uses: "actions/checkout@v3"
- name: "Install ${{ matrix.python-version }} with deadsnakes"
- # uses: deadsnakes/action@v2.1.1
- uses: deadsnakes/action@7ab8819e223c70d2bdedd692dfcea75824e0a617
+ uses: deadsnakes/action@e3117c2981fd8afe4af79f3e1be80066c82b70f5
if: "!startsWith(matrix.python-version, 'pypy-')"
with:
python-version: "${{ matrix.python-version }}"
@@ -77,7 +82,7 @@
- name: "Install dependencies"
run: |
- python -m pip install --require-hashes -r requirements/tox.pip
+ python -m pip install -r requirements/tox.pip
- name: "Run tox"
run: |
diff -Nru python-coverage-6.5.0+dfsg1/.github/workflows/quality.yml python-coverage-7.2.7+dfsg1/.github/workflows/quality.yml
--- python-coverage-6.5.0+dfsg1/.github/workflows/quality.yml 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/.github/workflows/quality.yml 2023-05-29 19:46:30.000000000 +0000
@@ -46,15 +46,37 @@
- name: "Install dependencies"
run: |
- set -xe
- python -VV
- python -m site
- python -m pip install --require-hashes -r requirements/tox.pip
+ python -m pip install -r requirements/tox.pip
- name: "Tox lint"
run: |
python -m tox -e lint
+ mypy:
+ name: "Check types"
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: "Check out the repo"
+ uses: "actions/checkout@v3"
+
+ - name: "Install Python"
+ uses: "actions/setup-python@v4"
+ with:
+ python-version: "3.8" # Minimum of PYVERSIONS, but at least 3.8
+ cache: pip
+ cache-dependency-path: 'requirements/*.pip'
+
+ - name: "Install dependencies"
+ run: |
+ # We run on 3.8, but the pins were made on 3.7, so don't insist on
+ # hashes, which won't match.
+ python -m pip install -r requirements/tox.pip
+
+ - name: "Tox mypy"
+ run: |
+ python -m tox -e mypy
+
doc:
name: "Build docs"
runs-on: ubuntu-latest
@@ -75,7 +97,7 @@
set -xe
python -VV
python -m site
- python -m pip install --require-hashes -r requirements/tox.pip
+ python -m pip install -r requirements/tox.pip
- name: "Tox doc"
run: |
diff -Nru python-coverage-6.5.0+dfsg1/.github/workflows/testsuite.yml python-coverage-7.2.7+dfsg1/.github/workflows/testsuite.yml
--- python-coverage-6.5.0+dfsg1/.github/workflows/testsuite.yml 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/.github/workflows/testsuite.yml 2023-05-29 19:46:30.000000000 +0000
@@ -18,6 +18,7 @@
env:
PIP_DISABLE_PIP_VERSION_CHECK: 1
COVERAGE_IGOR_VERBOSE: 1
+ FORCE_COLOR: 1 # Get colored pytest output
permissions:
contents: read
@@ -38,7 +39,7 @@
- macos
- windows
python-version:
- # When changing this list, be sure to check the [gh-actions] list in
+ # When changing this list, be sure to check the [gh] list in
# tox.ini so that tox will run properly. PYVERSIONS
# Available versions:
# https://github.com/actions/python-versions/blob/main/versions-manifest.json
@@ -47,9 +48,14 @@
- "3.8"
- "3.9"
- "3.10"
- - "3.11.0-rc.2"
+ - "3.11"
+ - "3.12"
- "pypy-3.7"
- "pypy-3.9"
+ exclude:
+ # Windows PyPy-3.9 always gets killed.
+ - os: windows
+ python-version: "pypy-3.9"
fail-fast: false
steps:
@@ -60,6 +66,7 @@
uses: "actions/setup-python@v4"
with:
python-version: "${{ matrix.python-version }}"
+ allow-prereleases: true
cache: pip
cache-dependency-path: 'requirements/*.pip'
@@ -68,36 +75,31 @@
set -xe
python -VV
python -m site
- python -m pip install --require-hashes -r requirements/tox.pip
+ python -m pip install -r requirements/tox.pip
# For extreme debugging:
# python -c "import urllib.request as r; exec(r.urlopen('https://bit.ly/pydoctor').read())"
- name: "Run tox for ${{ matrix.python-version }}"
- continue-on-error: true
- id: tox1
run: |
python -m tox -- -rfsEX
- name: "Retry tox for ${{ matrix.python-version }}"
- id: tox2
- if: steps.tox1.outcome == 'failure'
+ if: failure()
run: |
- python -m tox -- -rfsEX
-
- - name: "Set status"
- if: always()
- run: |
- if ${{ steps.tox1.outcome != 'success' && steps.tox2.outcome != 'success' }}; then
- exit 1
- fi
+ # `exit 1` makes sure that the job remains red with flaky runs
+ python -m tox -- -rfsEX --lf -vvvvv && exit 1
- # A final step to give a simple name for required status checks.
+ # This job aggregates test results. It's the required check for branch protection.
+ # https://github.com/marketplace/actions/alls-green#why
# https://github.com/orgs/community/discussions/33579
success:
- needs: tests
- runs-on: ubuntu-latest
name: Tests successful
+ if: always()
+ needs:
+ - tests
+ runs-on: ubuntu-latest
steps:
- - name: "Success"
- run: |
- echo Tests successful
+ - name: Decide whether the needed jobs succeeded or failed
+ uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe
+ with:
+ jobs: ${{ toJSON(needs) }}
diff -Nru python-coverage-6.5.0+dfsg1/howto.txt python-coverage-7.2.7+dfsg1/howto.txt
--- python-coverage-6.5.0+dfsg1/howto.txt 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/howto.txt 2023-05-29 19:46:30.000000000 +0000
@@ -1,55 +1,63 @@
* Release checklist
- Check that the current virtualenv matches the current coverage branch.
-- Version number in coverage/version.py
+- start branch for release work
+ $ make relbranch
+- Edit version number in coverage/version.py
version_info = (4, 0, 2, "alpha", 1)
version_info = (4, 0, 2, "beta", 1)
version_info = (4, 0, 2, "candidate", 1)
version_info = (4, 0, 2, "final", 0)
-- Supported Python version numbers. Search for "PYVERSIONS".
-- Copyright date in NOTICE.txt
-- run `python igor.py cheats` to get useful snippets for next steps.
-- Update CHANGES.rst, including release date.
- - don't forget the jump target
+ - make sure: _dev = 0
+- Edit supported Python version numbers. Search for "PYVERSIONS".
+ - Especially README.rst and doc/index.rst
+- Update source files with release facts:
+ $ make edit_for_release
+- Get useful snippets for next steps, and beyond, in cheats.txt
+ $ make cheats
+- Look over CHANGES.rst
- Update README.rst
- "New in x.y:"
- - Python versions supported
- Update docs
- - Python versions in doc/index.rst
- IF PRE-RELEASE:
- Version of latest stable release in doc/index.rst
- - Version, release, release_date and copyright date in doc/conf.py
- - Look for CHANGEME comments
- Make sure the docs are cogged:
$ make prebuild
- Don't forget the man page: doc/python-coverage.1.txt
- Check that the docs build correctly:
$ tox -e doc
- commit the release-prep changes
+ $ make relcommit1
- Generate new sample_html to get the latest, incl footer version number:
- IF PRE-RELEASE:
$ make sample_html_beta
- IF NOT PRE-RELEASE:
$ make sample_html
- check in the new sample html
-- Done with changes to source files, check them in.
- $ git push
+ - check in the new sample html
+ $ make relcommit2
+- Done with changes to source files
+ - check them in on the release prep branch
+ - wait for ci to finish
+ - merge to master
+ - git push
+- Start the kits:
+ - Trigger the kit GitHub Action
+ $ make build_kits
- Build and publish docs:
- IF PRE-RELEASE:
$ make publishbeta
- ELSE:
$ make publish
+ - commit and publish nedbatchelder.com
- Kits:
- - Trigger the kit GitHub Action
- $ make build_kits
- - wait for it to finish:
- - https://github.com/nedbat/coveragepy/actions/workflows/kit.yml
+ - Wait for kits to finish:
+ - https://github.com/nedbat/coveragepy/actions/workflows/kit.yml
- Download and check built kits from GitHub Actions:
$ make clean download_kits check_kits
- examine the dist directory, and remove anything that looks malformed.
+ - opvars
- test the pypi upload:
$ make test_upload
-- Update PyPI:
- upload kits:
$ make kit_upload
- Tag the tree
@@ -59,33 +67,25 @@
$ make update_stable
- Update GitHub releases:
$ make clean github_releases
+- Visit the fixed issues on GitHub and mention the version it was fixed in.
+ $ make comment_on_fixes
+- unopvars
- Bump version:
- - coverage/version.py
- - increment version number
- - IF NOT PRE-RELEASE:
- - set to alpha-0 if just released.
- - CHANGES.rst
- - add an "Unreleased" section to the top.
- $ git push
+ $ make bump_version
- Update readthedocs
- @ https://readthedocs.org/projects/coverage/versions/
- find the latest tag in the inactive list, edit it, make it active.
- readthedocs won't find the tag until a commit is made on master.
- keep just the latest version of each x.y release, make the rest active but hidden.
+ - pre-releases should be hidden
- IF NOT PRE-RELEASE:
- @ https://readthedocs.org/projects/coverage/builds/
- wait for the new tag build to finish successfully.
- @ https://readthedocs.org/dashboard/coverage/advanced/
- change the default version to the new version
-- Visit the fixed issues on GitHub and mention the version it was fixed in.
- $ make comment_on_fixes
- - "This is now released as part of [coverage 5.2](https://pypi.org/project/coverage/5.2)."
-- Announce:
- - twitter @coveragepy
- - nedbatchelder.com blog post?
+- Once CI passes, merge the bump-version branch to master and push it
+
- things to automate:
- - url to link to latest changes in docs
- - next version.py line
- readthedocs api to do the readthedocs changes
diff -Nru python-coverage-6.5.0+dfsg1/igor.py python-coverage-7.2.7+dfsg1/igor.py
--- python-coverage-6.5.0+dfsg1/igor.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/igor.py 2023-05-29 19:46:30.000000000 +0000
@@ -10,15 +10,18 @@
import contextlib
import datetime
-import fnmatch
import glob
import inspect
+import itertools
import os
import platform
+import pprint
+import re
import subprocess
import sys
import sysconfig
import textwrap
+import types
import warnings
import zipfile
@@ -75,10 +78,11 @@
"-c",
"import coverage; print(coverage.__file__)"
], encoding="utf-8").strip())
+ roots = [root]
else:
- root = "coverage"
+ roots = ["coverage", "build/*/coverage"]
- for pattern in so_patterns:
+ for root, pattern in itertools.product(roots, so_patterns):
pattern = os.path.join(root, pattern.strip())
if VERBOSITY:
print(f"Searching for {pattern}")
@@ -160,6 +164,8 @@
os.environ['COVERAGE_HOME'] = os.getcwd()
context = os.environ.get('COVERAGE_CONTEXT')
if context:
+ if context[0] == "$":
+ context = os.environ[context[1:]]
os.environ['COVERAGE_CONTEXT'] = context + "." + tracer
# Create the .pth file that will let us measure coverage in sub-processes.
@@ -203,25 +209,24 @@
cov.stop()
os.remove(pth_path)
- cov.combine()
cov.save()
-
return status
def do_combine_html():
- """Combine data from a meta-coverage run, and make the HTML and XML reports."""
+ """Combine data from a meta-coverage run, and make the HTML report."""
import coverage
os.environ['COVERAGE_HOME'] = os.getcwd()
- os.environ['COVERAGE_METAFILE'] = os.path.abspath(".metacov")
cov = coverage.Coverage(config_file="metacov.ini")
cov.load()
cov.combine()
cov.save()
+ # A new Coverage to turn on messages. Better would be to have tighter
+ # control over message verbosity...
+ cov = coverage.Coverage(config_file="metacov.ini", messages=True)
+ cov.load()
show_contexts = bool(os.environ.get('COVERAGE_DYNCTX') or os.environ.get('COVERAGE_CONTEXT'))
cov.html_report(show_contexts=show_contexts)
- cov.xml_report()
- cov.json_report(pretty_print=True)
def do_test_with_tracer(tracer, *runner_args):
@@ -277,77 +282,6 @@
zf.write("coverage/__main__.py", "__main__.py")
-def do_check_eol():
- """Check files for incorrect newlines and trailing whitespace."""
-
- ignore_dirs = [
- '.svn', '.hg', '.git',
- '.tox*',
- '*.egg-info',
- '_build',
- '_spell',
- 'tmp',
- 'help',
- ]
- checked = set()
-
- def check_file(fname, crlf=True, trail_white=True):
- """Check a single file for whitespace abuse."""
- fname = os.path.relpath(fname)
- if fname in checked:
- return
- checked.add(fname)
-
- line = None
- with open(fname, "rb") as f:
- for n, line in enumerate(f, start=1):
- if crlf:
- if b"\r" in line:
- print(f"{fname}@{n}: CR found")
- return
- if trail_white:
- line = line[:-1]
- if not crlf:
- line = line.rstrip(b'\r')
- if line.rstrip() != line:
- print(f"{fname}@{n}: trailing whitespace found")
- return
-
- if line is not None and not line.strip():
- print(f"{fname}: final blank line")
-
- def check_files(root, patterns, **kwargs):
- """Check a number of files for whitespace abuse."""
- for where, dirs, files in os.walk(root):
- for f in files:
- fname = os.path.join(where, f)
- for p in patterns:
- if fnmatch.fnmatch(fname, p):
- check_file(fname, **kwargs)
- break
- for ignore_dir in ignore_dirs:
- ignored = []
- for dir_name in dirs:
- if fnmatch.fnmatch(dir_name, ignore_dir):
- ignored.append(dir_name)
- for dir_name in ignored:
- dirs.remove(dir_name)
-
- check_files("coverage", ["*.py"])
- check_files("coverage/ctracer", ["*.c", "*.h"])
- check_files("coverage/htmlfiles", ["*.html", "*.scss", "*.css", "*.js"])
- check_files("tests", ["*.py"])
- check_files("tests", ["*,cover"], trail_white=False)
- check_files("tests/js", ["*.js", "*.html"])
- check_file("setup.py")
- check_file("igor.py")
- check_file("Makefile")
- check_files(".", ["*.rst", "*.txt"])
- check_files(".", ["*.pip"])
- check_files(".github", ["*"])
- check_files("ci", ["*"])
-
-
def print_banner(label):
"""Print the version of Python."""
try:
@@ -380,56 +314,113 @@
return proc.returncode
-def do_cheats():
- """Show a cheatsheet of useful things during releasing."""
+def get_release_facts():
+ """Return an object with facts about the current release."""
import coverage
- ver = coverage.__version__
- vi = coverage.version_info
- shortver = f"{vi[0]}.{vi[1]}.{vi[2]}"
- anchor = shortver.replace(".", "-")
- if vi[3] != "final":
- anchor += f"{vi[3][0]}{vi[4]}"
- now = datetime.datetime.now()
- branch = subprocess.getoutput("git rev-parse --abbrev-ref @")
- print(f"Coverage version is {ver}")
+ import coverage.version
+ facts = types.SimpleNamespace()
+ facts.ver = coverage.__version__
+ mjr, mnr, mcr, rel, ser = facts.vi = coverage.version_info
+ facts.dev = coverage.version._dev
+ facts.shortver = f"{mjr}.{mnr}.{mcr}"
+ facts.anchor = facts.shortver.replace(".", "-")
+ if rel == "final":
+ facts.next_vi = (mjr, mnr, mcr+1, "alpha", 0)
+ else:
+ facts.anchor += f"{rel[0]}{ser}"
+ facts.next_vi = (mjr, mnr, mcr, rel, ser + 1)
- print(f"pip install git+https://github.com/nedbat/coveragepy@{branch}")
- print(f"https://coverage.readthedocs.io/en/{ver}/changes.html#changes-{anchor}")
+ facts.now = datetime.datetime.now()
+ facts.branch = subprocess.getoutput("git rev-parse --abbrev-ref @")
+ facts.sha = subprocess.getoutput("git rev-parse @")
+ return facts
+
+
+def update_file(fname, pattern, replacement):
+ """Update the contents of a file, replacing pattern with replacement."""
+ with open(fname) as fobj:
+ old_text = fobj.read()
+
+ new_text = re.sub(pattern, replacement, old_text, count=1)
+
+ if new_text != old_text:
+ print(f"Updating {fname}")
+ with open(fname, "w") as fobj:
+ fobj.write(new_text)
+
+UNRELEASED = "Unreleased\n----------"
+SCRIV_START = ".. scriv-start-here\n\n"
+
+def do_edit_for_release():
+ """Edit a few files in preparation for a release."""
+ facts = get_release_facts()
+
+ if facts.dev:
+ print(f"**\n** This is a dev release: {facts.ver}\n**\n\nNo edits")
+ return
+
+ # NOTICE.txt
+ update_file("NOTICE.txt", r"Copyright 2004.*? Ned", f"Copyright 2004-{facts.now:%Y} Ned")
+
+ # CHANGES.rst
+ title = f"Version {facts.ver} — {facts.now:%Y-%m-%d}"
+ rule = "-" * len(title)
+ new_head = f".. _changes_{facts.anchor}:\n\n{title}\n{rule}"
+
+ update_file("CHANGES.rst", re.escape(SCRIV_START), "")
+ update_file("CHANGES.rst", re.escape(UNRELEASED), SCRIV_START + new_head)
+
+ # doc/conf.py
+ new_conf = textwrap.dedent(f"""\
+ # @@@ editable
+ copyright = "2009\N{EN DASH}{facts.now:%Y}, Ned Batchelder" # pylint: disable=redefined-builtin
+ # The short X.Y.Z version.
+ version = "{facts.shortver}"
+ # The full version, including alpha/beta/rc tags.
+ release = "{facts.ver}"
+ # The date of release, in "monthname day, year" format.
+ release_date = "{facts.now:%B %-d, %Y}"
+ # @@@ end
+ """)
+ update_file("doc/conf.py", r"(?s)# @@@ editable\n.*# @@@ end\n", new_conf)
+
+
+def do_bump_version():
+ """Edit a few files right after a release to bump the version."""
+ facts = get_release_facts()
+
+ # CHANGES.rst
+ update_file(
+ "CHANGES.rst",
+ re.escape(SCRIV_START),
+ f"{UNRELEASED}\n\nNothing yet.\n\n\n" + SCRIV_START,
+ )
- print("\n## for CHANGES.rst before release:")
- print(f".. _changes_{anchor}:")
+ # coverage/version.py
+ next_version = f"version_info = {facts.next_vi}\n_dev = 1".replace("'", '"')
+ update_file("coverage/version.py", r"(?m)^version_info = .*\n_dev = \d+$", next_version)
+
+
+def do_cheats():
+ """Show a cheatsheet of useful things during releasing."""
+ facts = get_release_facts()
+ pprint.pprint(facts.__dict__)
print()
- head = f"Version {ver} — {now:%Y-%m-%d}"
- print(head)
- print("-" * len(head))
-
- print("\n## For doc/conf.py before release:")
- print("\n".join([
- '# The short X.Y.Z version. # CHANGEME',
- f'version = "{shortver}"',
- '# The full version, including alpha/beta/rc tags. # CHANGEME',
- f'release = "{ver}"',
- '# The date of release, in "monthname day, year" format. # CHANGEME',
- f'release_date = "{now:%B %-d, %Y}"',
- ]))
+ print(f"Coverage version is {facts.ver}")
+
+ egg = "egg=coverage==0.0" # to force a re-install
+ if facts.branch == "master":
+ print(f"pip install git+https://github.com/nedbat/coveragepy#{egg}")
+ else:
+ print(f"pip install git+https://github.com/nedbat/coveragepy@{facts.branch}#{egg}")
+ print(f"pip install git+https://github.com/nedbat/coveragepy@{facts.sha}#{egg}")
+ print(f"https://coverage.readthedocs.io/en/{facts.ver}/changes.html#changes-{facts.anchor}")
print(
"\n## For GitHub commenting:\n" +
"This is now released as part of " +
- f"[coverage {ver}](https://pypi.org/project/coverage/{ver})."
+ f"[coverage {facts.ver}](https://pypi.org/project/coverage/{facts.ver})."
)
- print("\n## For version.py next:")
- next_vi = (vi[0], vi[1], vi[2]+1, "alpha", 0)
- print(f"version_info = {next_vi}".replace("'", '"'))
- print("\n## For CHANGES.rst after release:")
- print(textwrap.dedent("""\
- Unreleased
- ----------
-
- Nothing yet.
-
-
- """))
def do_help():
diff -Nru python-coverage-6.5.0+dfsg1/lab/benchmark/benchmark.py python-coverage-7.2.7+dfsg1/lab/benchmark/benchmark.py
--- python-coverage-6.5.0+dfsg1/lab/benchmark/benchmark.py 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/lab/benchmark/benchmark.py 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +1,584 @@
+"""Run performance comparisons for versions of coverage"""
+
+import collections
+import contextlib
+import dataclasses
+import itertools
+import os
+import random
+import shutil
+import statistics
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple
+
+
+class ShellSession:
+ """A logged shell session.
+
+ The duration of the last command is available as .last_duration.
+ """
+
+ def __init__(self, output_filename: str):
+ self.output_filename = output_filename
+ self.last_duration: float = 0
+ self.foutput = None
+
+ def __enter__(self):
+ self.foutput = open(self.output_filename, "a", encoding="utf-8")
+ print(f"Logging output to {os.path.abspath(self.output_filename)}")
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.foutput.close()
+
+ def print(self, *args, **kwargs):
+ """Print a message to this shell's log."""
+ print(*args, **kwargs, file=self.foutput)
+
+ def run_command(self, cmd: str) -> str:
+ """
+ Run a command line (with a shell).
+
+ Returns:
+ str: the output of the command.
+
+ """
+ self.print(f"\n========================\n$ {cmd}")
+ start = time.perf_counter()
+ proc = subprocess.run(
+ cmd,
+ shell=True,
+ check=False,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ )
+ output = proc.stdout.decode("utf-8")
+ self.last_duration = time.perf_counter() - start
+ self.print(output, end="")
+ self.print(f"(was: {cmd})")
+ self.print(f"(in {os.getcwd()}, duration: {self.last_duration:.3f}s)")
+
+ if proc.returncode != 0:
+ self.print(f"ERROR: command returned {proc.returncode}")
+ raise Exception(
+ f"Command failed ({proc.returncode}): {cmd!r}, output was:\n{output}"
+ )
+
+ return output.strip()
+
+
+def rmrf(path: Path) -> None:
+ """
+ Remove a directory tree. It's OK if it doesn't exist.
+ """
+ if path.exists():
+ shutil.rmtree(path)
+
+
+@contextlib.contextmanager
+def change_dir(newdir: Path) -> Iterator[Path]:
+ """
+ Change to a new directory, and then change back.
+
+ Will make the directory if needed.
+ """
+ old_dir = os.getcwd()
+ newdir.mkdir(parents=True, exist_ok=True)
+ os.chdir(newdir)
+ try:
+ yield newdir
+ finally:
+ os.chdir(old_dir)
+
+
+@contextlib.contextmanager
+def file_replace(file_name: Path, old_text: str, new_text: str) -> Iterator[None]:
+ """
+ Replace some text in `file_name`, and change it back.
+ """
+ if old_text:
+ file_text = file_name.read_text()
+ if old_text not in file_text:
+ raise Exception("Old text {old_text!r} not found in {file_name}")
+ updated_text = file_text.replace(old_text, new_text)
+ file_name.write_text(updated_text)
+ try:
+ yield
+ finally:
+ if old_text:
+ file_name.write_text(file_text)
+
+
+class ProjectToTest:
+ """Information about a project to use as a test case."""
+
+ # Where can we clone the project from?
+ git_url: Optional[str] = None
+ slug: Optional[str] = None
+
+ def __init__(self):
+ if not self.slug:
+ if self.git_url:
+ self.slug = self.git_url.split("/")[-1]
+
+ def shell(self):
+ return ShellSession(f"output_{self.slug}.log")
+
+ def make_dir(self):
+ self.dir = Path(f"work_{self.slug}")
+ if self.dir.exists():
+ rmrf(self.dir)
+
+ def get_source(self, shell):
+ """Get the source of the project."""
+ shell.run_command(f"git clone {self.git_url} {self.dir}")
+
+ def prep_environment(self, env):
+ """Prepare the environment to run the test suite.
+
+ This is not timed.
+ """
+ pass
+
+ def tweak_coverage_settings(
+ self, settings: Iterable[Tuple[str, Any]]
+ ) -> Iterator[None]:
+ """Tweak the coverage settings.
+
+ NOTE: This is not properly factored, and is only used by ToxProject now!!!
+ """
+ pass
+
+ def run_no_coverage(self, env):
+ """Run the test suite with no coverage measurement."""
+ pass
+
+ def run_with_coverage(self, env, pip_args, cov_tweaks):
+ """Run the test suite with coverage measurement."""
+ pass
+
+
+class EmptyProject(ProjectToTest):
+ """A dummy project for testing other parts of this code."""
+
+ def __init__(self, slug: str = "empty", fake_durations: Iterable[float] = (1.23,)):
+ self.slug = slug
+ self.durations = iter(itertools.cycle(fake_durations))
+
+ def get_source(self, shell):
+ pass
+
+ def run_with_coverage(self, env, pip_args, cov_tweaks):
+ """Run the test suite with coverage measurement."""
+ return next(self.durations)
+
+
+class ToxProject(ProjectToTest):
+ """A project using tox to run the test suite."""
+
+ def prep_environment(self, env):
+ env.shell.run_command(f"{env.python} -m pip install 'tox<4'")
+ self.run_tox(env, env.pyver.toxenv, "--notest")
+
+ def run_tox(self, env, toxenv, toxargs=""):
+ """Run a tox command. Return the duration."""
+ env.shell.run_command(f"{env.python} -m tox -e {toxenv} {toxargs}")
+ return env.shell.last_duration
+
+ def run_no_coverage(self, env):
+ return self.run_tox(env, env.pyver.toxenv, "--skip-pkg-install")
+
+ def run_with_coverage(self, env, pip_args, cov_tweaks):
+ self.run_tox(env, env.pyver.toxenv, "--notest")
+ env.shell.run_command(
+ f".tox/{env.pyver.toxenv}/bin/python -m pip install {pip_args}"
+ )
+ with self.tweak_coverage_settings(cov_tweaks):
+ self.pre_check(env) # NOTE: Not properly factored, and only used from here.
+ duration = self.run_tox(env, env.pyver.toxenv, "--skip-pkg-install")
+ self.post_check(
+ env
+ ) # NOTE: Not properly factored, and only used from here.
+ return duration
+
+
+class ProjectPytestHtml(ToxProject):
+ """pytest-dev/pytest-html"""
+
+ git_url = "https://github.com/pytest-dev/pytest-html"
+
+ def run_with_coverage(self, env, pip_args, cov_tweaks):
+ raise Exception("This doesn't work because options changed to tweaks")
+ covenv = env.pyver.toxenv + "-cov"
+ self.run_tox(env, covenv, "--notest")
+ env.shell.run_command(f".tox/{covenv}/bin/python -m pip install {pip_args}")
+ if cov_tweaks:
+ replace = ("# reference: https", f"[run]\n{cov_tweaks}\n#")
+ else:
+ replace = ("", "")
+ with file_replace(Path(".coveragerc"), *replace):
+ env.shell.run_command("cat .coveragerc")
+ env.shell.run_command(f".tox/{covenv}/bin/python -m coverage debug sys")
+ return self.run_tox(env, covenv, "--skip-pkg-install")
+
+
+class ProjectDateutil(ToxProject):
+ """dateutil/dateutil"""
+
+ git_url = "https://github.com/dateutil/dateutil"
+
+ def prep_environment(self, env):
+ super().prep_environment(env)
+ env.shell.run_command(f"{env.python} updatezinfo.py")
+
+ def run_no_coverage(self, env):
+ env.shell.run_command("echo No option to run without coverage")
+ return 0
+
+
+class ProjectAttrs(ToxProject):
+ """python-attrs/attrs"""
+
+ git_url = "https://github.com/python-attrs/attrs"
+
+ def tweak_coverage_settings(
+ self, tweaks: Iterable[Tuple[str, Any]]
+ ) -> Iterator[None]:
+ return tweak_toml_coverage_settings("pyproject.toml", tweaks)
+
+ def pre_check(self, env):
+ env.shell.run_command("cat pyproject.toml")
+
+ def post_check(self, env):
+ env.shell.run_command("ls -al")
+
+
+def tweak_toml_coverage_settings(
+ toml_file: str, tweaks: Iterable[Tuple[str, Any]]
+) -> Iterator[None]:
+ if tweaks:
+ toml_inserts = []
+ for name, value in tweaks:
+ if isinstance(value, bool):
+ toml_inserts.append(f"{name} = {str(value).lower()}")
+ elif isinstance(value, str):
+ toml_inserts.append(f"{name} = '{value}'")
+ else:
+ raise Exception(f"Can't tweak toml setting: {name} = {value!r}")
+ header = "[tool.coverage.run]\n"
+ insert = header + "\n".join(toml_inserts) + "\n"
+ else:
+ header = insert = ""
+ return file_replace(Path(toml_file), header, insert)
+
+
+class AdHocProject(ProjectToTest):
+ """A standalone program to run locally."""
+
+ def __init__(self, python_file, cur_dir=None, pip_args=None):
+ super().__init__()
+ self.python_file = Path(python_file)
+ if not self.python_file.exists():
+ raise ValueError(f"Couldn't find {self.python_file} to run ad-hoc.")
+ self.cur_dir = Path(cur_dir or self.python_file.parent)
+ if not self.cur_dir.exists():
+ raise ValueError(f"Couldn't find {self.cur_dir} to run in.")
+ self.pip_args = pip_args
+ self.slug = self.python_file.name
+
+ def get_source(self, shell):
+ pass
+
+ def prep_environment(self, env):
+ env.shell.run_command(f"{env.python} -m pip install {self.pip_args}")
+
+ def run_no_coverage(self, env):
+ with change_dir(self.cur_dir):
+ env.shell.run_command(f"{env.python} {self.python_file}")
+ return env.shell.last_duration
+
+ def run_with_coverage(self, env, pip_args, cov_tweaks):
+ env.shell.run_command(f"{env.python} -m pip install {pip_args}")
+ with change_dir(self.cur_dir):
+ env.shell.run_command(f"{env.python} -m coverage run {self.python_file}")
+ return env.shell.last_duration
+
+
+class SlipcoverBenchmark(AdHocProject):
+ """
+ For running code from the Slipcover benchmarks.
+
+ Clone https://github.com/plasma-umass/slipcover to /src/slipcover
+
+ """
+
+ def __init__(self, python_file):
+ super().__init__(
+ python_file=f"/src/slipcover/benchmarks/{python_file}",
+ cur_dir="/src/slipcover",
+ pip_args="six pyperf",
+ )
+
+
+class PyVersion:
+ """A version of Python to use."""
+
+ # The command to run this Python
+ command: str
+ # Short word for messages, directories, etc
+ slug: str
+ # The tox environment to run this Python
+ toxenv: str
+
+
+class Python(PyVersion):
+ """A version of CPython to use."""
+
+ def __init__(self, major, minor):
+ self.command = self.slug = f"python{major}.{minor}"
+ self.toxenv = f"py{major}{minor}"
+
+
+class PyPy(PyVersion):
+ """A version of PyPy to use."""
+
+ def __init__(self, major, minor):
+ self.command = self.slug = f"pypy{major}.{minor}"
+ self.toxenv = f"pypy{major}{minor}"
+
+
+class AdHocPython(PyVersion):
+ """A custom build of Python to use."""
+
+ def __init__(self, path, slug):
+ self.command = f"{path}/bin/python3"
+ self.slug = slug
+ self.toxenv = None
+
+
+@dataclasses.dataclass
+class Coverage:
+ """A version of coverage.py to use, maybe None."""
+
+ # Short word for messages, directories, etc
+ slug: str
+ # Arguments for "pip install ..."
+ pip_args: Optional[str] = None
+ # Tweaks to the .coveragerc file
+ tweaks: Optional[Iterable[Tuple[str, Any]]] = None
+
+
+class CoveragePR(Coverage):
+ """A version of coverage.py from a pull request."""
+
+ def __init__(self, number, tweaks=None):
+ super().__init__(
+ slug=f"#{number}",
+ pip_args=f"git+https://github.com/nedbat/coveragepy.git@refs/pull/{number}/merge",
+ tweaks=tweaks,
+ )
+
+
+class CoverageCommit(Coverage):
+ """A version of coverage.py from a specific commit."""
+
+ def __init__(self, sha, tweaks=None):
+ super().__init__(
+ slug=sha,
+ pip_args=f"git+https://github.com/nedbat/coveragepy.git@{sha}",
+ tweaks=tweaks,
+ )
+
+
+class CoverageSource(Coverage):
+ """The coverage.py in a working tree."""
+
+ def __init__(self, directory, tweaks=None):
+ super().__init__(
+ slug="source",
+ pip_args=directory,
+ tweaks=tweaks,
+ )
+
+
+@dataclasses.dataclass
+class Env:
+ """An environment to run a test suite in."""
+
+ pyver: PyVersion
+ python: Path
+ shell: ShellSession
+
+
+ResultKey = Tuple[str, str, str]
+
+DIMENSION_NAMES = ["proj", "pyver", "cov"]
+
+
+class Experiment:
+ """A particular time experiment to run."""
+
+ def __init__(
+ self,
+ py_versions: List[PyVersion],
+ cov_versions: List[Coverage],
+ projects: List[ProjectToTest],
+ ):
+ self.py_versions = py_versions
+ self.cov_versions = cov_versions
+ self.projects = projects
+ self.result_data: Dict[ResultKey, List[float]] = {}
+
+ def run(self, num_runs: int = 3) -> None:
+ total_runs = (
+ len(self.projects)
+ * len(self.py_versions)
+ * len(self.cov_versions)
+ * num_runs
+ )
+ total_run_nums = iter(itertools.count(start=1))
+
+ all_runs = []
+
+ for proj in self.projects:
+ print(f"Prepping project {proj.slug}")
+ with proj.shell() as shell:
+ proj.make_dir()
+ proj.get_source(shell)
+
+ for pyver in self.py_versions:
+ print(f"Making venv for {proj.slug} {pyver.slug}")
+ venv_dir = f"venv_{proj.slug}_{pyver.slug}"
+ shell.run_command(f"{pyver.command} -m venv {venv_dir}")
+ python = Path.cwd() / f"{venv_dir}/bin/python"
+ shell.run_command(f"{python} -V")
+ env = Env(pyver, python, shell)
+
+ with change_dir(proj.dir):
+ print(f"Prepping for {proj.slug} {pyver.slug}")
+ proj.prep_environment(env)
+ for cov_ver in self.cov_versions:
+ all_runs.append((proj, pyver, cov_ver, env))
+
+ all_runs *= num_runs
+ random.shuffle(all_runs)
+
+ run_data: Dict[ResultKey, List[float]] = collections.defaultdict(list)
+
+ for proj, pyver, cov_ver, env in all_runs:
+ total_run_num = next(total_run_nums)
+ print(
+ "Running tests: "
+ + f"{proj.slug}, {pyver.slug}, cov={cov_ver.slug}, "
+ + f"{total_run_num} of {total_runs}"
+ )
+ with env.shell:
+ with change_dir(proj.dir):
+ if cov_ver.pip_args is None:
+ dur = proj.run_no_coverage(env)
+ else:
+ dur = proj.run_with_coverage(
+ env,
+ cov_ver.pip_args,
+ cov_ver.tweaks,
+ )
+ print(f"Tests took {dur:.3f}s")
+ result_key = (proj.slug, pyver.slug, cov_ver.slug)
+ run_data[result_key].append(dur)
+
+ # Summarize and collect the data.
+ print("# Results")
+ for proj in self.projects:
+ for pyver in self.py_versions:
+ for cov_ver in self.cov_versions:
+ result_key = (proj.slug, pyver.slug, cov_ver.slug)
+ med = statistics.median(run_data[result_key])
+ self.result_data[result_key] = med
+ print(
+ f"Median for {proj.slug}, {pyver.slug}, "
+ + f"cov={cov_ver.slug}: {med:.3f}s"
+ )
+
+ def show_results(
+ self,
+ rows: List[str],
+ column: str,
+ ratios: Iterable[Tuple[str, str, str]] = (),
+ ) -> None:
+ dimensions = {
+ "cov": [cov_ver.slug for cov_ver in self.cov_versions],
+ "pyver": [pyver.slug for pyver in self.py_versions],
+ "proj": [proj.slug for proj in self.projects],
+ }
+
+ table_axes = [dimensions[rowname] for rowname in rows]
+ data_order = [*rows, column]
+ remap = [data_order.index(datum) for datum in DIMENSION_NAMES]
+
+ WIDTH = 20
+
+ def as_table_row(vals):
+ return "| " + " | ".join(v.ljust(WIDTH) for v in vals) + " |"
+
+ header = []
+ header.extend(rows)
+ header.extend(dimensions[column])
+ header.extend(slug for slug, _, _ in ratios)
+
+ print()
+ print(as_table_row(header))
+ dashes = [":---"] * len(rows) + ["---:"] * (len(header) - len(rows))
+ print(as_table_row(dashes))
+ for tup in itertools.product(*table_axes):
+ row = []
+ row.extend(tup)
+ col_data = {}
+ for col in dimensions[column]:
+ key = (*tup, col)
+ key = tuple(key[i] for i in remap)
+ result_time = self.result_data[key] # type: ignore
+ row.append(f"{result_time:.1f} s")
+ col_data[col] = result_time
+ for _, num, denom in ratios:
+ ratio = col_data[num] / col_data[denom]
+ row.append(f"{ratio * 100:.0f}%")
+ print(as_table_row(row))
+
+
+PERF_DIR = Path("/tmp/covperf")
+
+
+def run_experiment(
+ py_versions: List[PyVersion],
+ cov_versions: List[Coverage],
+ projects: List[ProjectToTest],
+ rows: List[str],
+ column: str,
+ ratios: Iterable[Tuple[str, str, str]] = (),
+):
+ slugs = [v.slug for v in py_versions + cov_versions + projects]
+ if len(set(slugs)) != len(slugs):
+ raise Exception(f"Slugs must be unique: {slugs}")
+ if any(" " in slug for slug in slugs):
+ raise Exception(f"No spaces in slugs please: {slugs}")
+ ratio_slugs = [rslug for ratio in ratios for rslug in ratio[1:]]
+ if any(rslug not in slugs for rslug in ratio_slugs):
+ raise Exception(f"Ratio slug doesn't match a slug: {ratio_slugs}, {slugs}")
+ if set(rows + [column]) != set(DIMENSION_NAMES):
+ raise Exception(
+ f"All of these must be in rows or column: {', '.join(DIMENSION_NAMES)}"
+ )
+
+ print(f"Removing and re-making {PERF_DIR}")
+ rmrf(PERF_DIR)
+
+ with change_dir(PERF_DIR):
+ exp = Experiment(
+ py_versions=py_versions, cov_versions=cov_versions, projects=projects
+ )
+ exp.run(num_runs=int(sys.argv[1]))
+ exp.show_results(rows=rows, column=column, ratios=ratios)
diff -Nru python-coverage-6.5.0+dfsg1/lab/benchmark/empty.py python-coverage-7.2.7+dfsg1/lab/benchmark/empty.py
--- python-coverage-6.5.0+dfsg1/lab/benchmark/empty.py 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/lab/benchmark/empty.py 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +1,29 @@
+from benchmark import *
+
+run_experiment(
+ py_versions=[
+ Python(3, 9),
+ Python(3, 11),
+ ],
+ cov_versions=[
+ Coverage("701", "coverage==7.0.1"),
+ Coverage(
+ "701.dynctx", "coverage==7.0.1", [("dynamic_context", "test_function")]
+ ),
+ Coverage("702", "coverage==7.0.2"),
+ Coverage(
+ "702.dynctx", "coverage==7.0.2", [("dynamic_context", "test_function")]
+ ),
+ ],
+ projects=[
+ EmptyProject("empty", [1.2, 3.4]),
+ EmptyProject("dummy", [6.9, 7.1]),
+ ],
+ rows=["proj", "pyver"],
+ column="cov",
+ ratios=[
+ (".2 vs .1", "702", "701"),
+ (".1 dynctx cost", "701.dynctx", "701"),
+ (".2 dynctx cost", "702.dynctx", "702"),
+ ],
+)
diff -Nru python-coverage-6.5.0+dfsg1/lab/benchmark/run.py python-coverage-7.2.7+dfsg1/lab/benchmark/run.py
--- python-coverage-6.5.0+dfsg1/lab/benchmark/run.py 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/lab/benchmark/run.py 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +1,54 @@
+from benchmark import *
+
+if 0:
+ run_experiment(
+ py_versions=[
+ # Python(3, 11),
+ AdHocPython("/usr/local/cpython/v3.10.5", "v3.10.5"),
+ AdHocPython("/usr/local/cpython/v3.11.0b3", "v3.11.0b3"),
+ AdHocPython("/usr/local/cpython/94231", "94231"),
+ ],
+ cov_versions=[
+ Coverage("6.4.1", "coverage==6.4.1"),
+ ],
+ projects=[
+ AdHocProject("/src/bugs/bug1339/bug1339.py"),
+ SlipcoverBenchmark("bm_sudoku.py"),
+ SlipcoverBenchmark("bm_spectral_norm.py"),
+ ],
+ rows=["cov", "proj"],
+ column="pyver",
+ ratios=[
+ ("3.11b3 vs 3.10", "v3.11.0b3", "v3.10.5"),
+ ("94231 vs 3.10", "94231", "v3.10.5"),
+ ],
+ )
+
+
+if 1:
+ run_experiment(
+ py_versions=[
+ Python(3, 9),
+ Python(3, 11),
+ ],
+ cov_versions=[
+ Coverage("701", "coverage==7.0.1"),
+ Coverage(
+ "701.dynctx", "coverage==7.0.1", [("dynamic_context", "test_function")]
+ ),
+ Coverage("702", "coverage==7.0.2"),
+ Coverage(
+ "702.dynctx", "coverage==7.0.2", [("dynamic_context", "test_function")]
+ ),
+ ],
+ projects=[
+ ProjectAttrs(),
+ ],
+ rows=["proj", "pyver"],
+ column="cov",
+ ratios=[
+ (".2 vs .1", "702", "701"),
+ (".1 dynctx cost", "701.dynctx", "701"),
+ (".2 dynctx cost", "702.dynctx", "702"),
+ ],
+ )
diff -Nru python-coverage-6.5.0+dfsg1/lab/benchmark.py python-coverage-7.2.7+dfsg1/lab/benchmark.py
--- python-coverage-6.5.0+dfsg1/lab/benchmark.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/lab/benchmark.py 1970-01-01 00:00:00.000000000 +0000
@@ -1,491 +0,0 @@
-"""Run performance comparisons for versions of coverage"""
-
-import contextlib
-import dataclasses
-import itertools
-import os
-import shutil
-import statistics
-import subprocess
-import sys
-import time
-from pathlib import Path
-
-from typing import Dict, Iterable, Iterator, List, Optional, Tuple
-
-
-class ShellSession:
- """A logged shell session.
-
- The duration of the last command is available as .last_duration.
- """
-
- def __init__(self, output_filename: str):
- self.output_filename = output_filename
- self.last_duration: float = 0
- self.foutput = None
-
- def __enter__(self):
- self.foutput = open(self.output_filename, "a", encoding="utf-8")
- print(f"Logging output to {os.path.abspath(self.output_filename)}")
- return self
-
- def __exit__(self, exc_type, exc_value, traceback):
- self.foutput.close()
-
- def print(self, *args, **kwargs):
- """Print a message to this shell's log."""
- print(*args, **kwargs, file=self.foutput)
-
- def run_command(self, cmd: str) -> str:
- """
- Run a command line (with a shell).
-
- Returns:
- str: the output of the command.
-
- """
- self.print(f"\n========================\n$ {cmd}")
- start = time.perf_counter()
- proc = subprocess.run(
- cmd,
- shell=True,
- check=False,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- )
- output = proc.stdout.decode("utf-8")
- self.last_duration = time.perf_counter() - start
- self.print(output, end="")
- self.print(f"(was: {cmd})")
- self.print(f"(in {os.getcwd()}, duration: {self.last_duration:.3f}s)")
-
- if proc.returncode != 0:
- self.print(f"ERROR: command returned {proc.returncode}")
- raise Exception(
- f"Command failed ({proc.returncode}): {cmd!r}, output was:\n{output}"
- )
-
- return output.strip()
-
-
-def rmrf(path: Path) -> None:
- """
- Remove a directory tree. It's OK if it doesn't exist.
- """
- if path.exists():
- shutil.rmtree(path)
-
-
-@contextlib.contextmanager
-def change_dir(newdir: Path) -> Iterator[Path]:
- """
- Change to a new directory, and then change back.
-
- Will make the directory if needed.
- """
- old_dir = os.getcwd()
- newdir.mkdir(parents=True, exist_ok=True)
- os.chdir(newdir)
- try:
- yield newdir
- finally:
- os.chdir(old_dir)
-
-
-@contextlib.contextmanager
-def file_replace(file_name: Path, old_text: str, new_text: str) -> Iterator[None]:
- """
- Replace some text in `file_name`, and change it back.
- """
- if old_text:
- file_text = file_name.read_text()
- if old_text not in file_text:
- raise Exception("Old text {old_text!r} not found in {file_name}")
- updated_text = file_text.replace(old_text, new_text)
- file_name.write_text(updated_text)
- try:
- yield
- finally:
- if old_text:
- file_name.write_text(file_text)
-
-
-class ProjectToTest:
- """Information about a project to use as a test case."""
-
- # Where can we clone the project from?
- git_url: Optional[str] = None
-
- def __init__(self):
- if self.git_url:
- self.slug = self.git_url.split("/")[-1]
- self.dir = Path(self.slug)
-
- def get_source(self, shell):
- """Get the source of the project."""
- if self.dir.exists():
- rmrf(self.dir)
- shell.run_command(f"git clone {self.git_url}")
-
- def prep_environment(self, env):
- """Prepare the environment to run the test suite.
-
- This is not timed.
- """
- pass
-
- def run_no_coverage(self, env):
- """Run the test suite with no coverage measurement."""
- pass
-
- def run_with_coverage(self, env, pip_args, cov_options):
- """Run the test suite with coverage measurement."""
- pass
-
-
-class ToxProject(ProjectToTest):
- """A project using tox to run the test suite."""
-
- def prep_environment(self, env):
- env.shell.run_command(f"{env.python} -m pip install tox")
- self.run_tox(env, env.pyver.toxenv, "--notest")
-
- def run_tox(self, env, toxenv, toxargs=""):
- """Run a tox command. Return the duration."""
- env.shell.run_command(f"{env.python} -m tox -e {toxenv} {toxargs}")
- return env.shell.last_duration
-
- def run_no_coverage(self, env):
- return self.run_tox(env, env.pyver.toxenv, "--skip-pkg-install")
-
- def run_with_coverage(self, env, pip_args, cov_options):
- assert not cov_options, f"ToxProject.run_with_coverage can't take cov_options={cov_options!r}"
- self.run_tox(env, env.pyver.toxenv, "--notest")
- env.shell.run_command(
- f".tox/{env.pyver.toxenv}/bin/python -m pip install {pip_args}"
- )
- return self.run_tox(env, env.pyver.toxenv, "--skip-pkg-install")
-
-
-class ProjectPytestHtml(ToxProject):
- """pytest-dev/pytest-html"""
-
- git_url = "https://github.com/pytest-dev/pytest-html"
-
- def run_with_coverage(self, env, pip_args, cov_options):
- covenv = env.pyver.toxenv + "-cov"
- self.run_tox(env, covenv, "--notest")
- env.shell.run_command(f".tox/{covenv}/bin/python -m pip install {pip_args}")
- if cov_options:
- replace = ("# reference: https", f"[run]\n{cov_options}\n#")
- else:
- replace = ("", "")
- with file_replace(Path(".coveragerc"), *replace):
- env.shell.run_command("cat .coveragerc")
- env.shell.run_command(f".tox/{covenv}/bin/python -m coverage debug sys")
- return self.run_tox(env, covenv, "--skip-pkg-install")
-
-
-class ProjectDateutil(ToxProject):
- """dateutil/dateutil"""
-
- git_url = "https://github.com/dateutil/dateutil"
-
- def prep_environment(self, env):
- super().prep_environment(env)
- env.shell.run_command(f"{env.python} updatezinfo.py")
-
- def run_no_coverage(self, env):
- env.shell.run_command("echo No option to run without coverage")
- return 0
-
-
-class ProjectAttrs(ToxProject):
- """python-attrs/attrs"""
-
- git_url = "https://github.com/python-attrs/attrs"
-
-
-class AdHocProject(ProjectToTest):
- """A standalone program to run locally."""
-
- def __init__(self, python_file, cur_dir=None, pip_args=None):
- super().__init__()
- self.python_file = Path(python_file)
- if not self.python_file.exists():
- raise ValueError(f"Couldn't find {self.python_file} to run ad-hoc.")
- self.cur_dir = Path(cur_dir or self.python_file.parent)
- if not self.cur_dir.exists():
- raise ValueError(f"Couldn't find {self.cur_dir} to run in.")
- self.pip_args = pip_args
- self.slug = self.python_file.name
-
- def get_source(self, shell):
- pass
-
- def prep_environment(self, env):
- env.shell.run_command(f"{env.python} -m pip install {self.pip_args}")
-
- def run_no_coverage(self, env):
- with change_dir(self.cur_dir):
- env.shell.run_command(f"{env.python} {self.python_file}")
- return env.shell.last_duration
-
- def run_with_coverage(self, env, pip_args, cov_options):
- env.shell.run_command(f"{env.python} -m pip install {pip_args}")
- with change_dir(self.cur_dir):
- env.shell.run_command(
- f"{env.python} -m coverage run {self.python_file}"
- )
- return env.shell.last_duration
-
-
-class SlipcoverBenchmark(AdHocProject):
- """
- For running code from the Slipcover benchmarks.
-
- Clone https://github.com/plasma-umass/slipcover to /src/slipcover
-
- """
- def __init__(self, python_file):
- super().__init__(
- python_file=f"/src/slipcover/benchmarks/{python_file}",
- cur_dir="/src/slipcover",
- pip_args="six pyperf",
- )
-
-class PyVersion:
- """A version of Python to use."""
-
- # The command to run this Python
- command: str
- # Short word for messages, directories, etc
- slug: str
- # The tox environment to run this Python
- toxenv: str
-
-
-class Python(PyVersion):
- """A version of CPython to use."""
-
- def __init__(self, major, minor):
- self.command = self.slug = f"python{major}.{minor}"
- self.toxenv = f"py{major}{minor}"
-
-
-class PyPy(PyVersion):
- """A version of PyPy to use."""
-
- def __init__(self, major, minor):
- self.command = self.slug = f"pypy{major}.{minor}"
- self.toxenv = f"pypy{major}{minor}"
-
-class AdHocPython(PyVersion):
- """A custom build of Python to use."""
- def __init__(self, path, slug):
- self.command = f"{path}/bin/python3"
- self.slug = slug
- self.toxenv = None
-
-@dataclasses.dataclass
-class Coverage:
- """A version of coverage.py to use, maybe None."""
- # Short word for messages, directories, etc
- slug: str
- # Arguments for "pip install ..."
- pip_args: Optional[str] = None
- # Tweaks to the .coveragerc file
- options: Optional[str] = None
-
-class CoveragePR(Coverage):
- """A version of coverage.py from a pull request."""
- def __init__(self, number, options=None):
- super().__init__(
- slug=f"#{number}",
- pip_args=f"git+https://github.com/nedbat/coveragepy.git@refs/pull/{number}/merge",
- options=options,
- )
-
-class CoverageCommit(Coverage):
- """A version of coverage.py from a specific commit."""
- def __init__(self, sha, options=None):
- super().__init__(
- slug=sha,
- pip_args=f"git+https://github.com/nedbat/coveragepy.git@{sha}",
- options=options,
- )
-
-class CoverageSource(Coverage):
- """The coverage.py in a working tree."""
- def __init__(self, directory, options=None):
- super().__init__(
- slug="source",
- pip_args=directory,
- options=options,
- )
-
-
-@dataclasses.dataclass
-class Env:
- """An environment to run a test suite in."""
-
- pyver: PyVersion
- python: Path
- shell: ShellSession
-
-
-ResultData = Dict[Tuple[str, str, str], float]
-
-class Experiment:
- """A particular time experiment to run."""
-
- def __init__(
- self,
- py_versions: List[PyVersion],
- cov_versions: List[Coverage],
- projects: List[ProjectToTest],
- ):
- self.py_versions = py_versions
- self.cov_versions = cov_versions
- self.projects = projects
- self.result_data: ResultData = {}
-
- def run(self, num_runs: int = 3) -> None:
- results = []
- for proj in self.projects:
- print(f"Testing with {proj.slug}")
- with ShellSession(f"output_{proj.slug}.log") as shell:
- proj.get_source(shell)
-
- for pyver in self.py_versions:
- print(f"Making venv for {proj.slug} {pyver.slug}")
- venv_dir = f"venv_{proj.slug}_{pyver.slug}"
- shell.run_command(f"{pyver.command} -m venv {venv_dir}")
- python = Path.cwd() / f"{venv_dir}/bin/python"
- shell.run_command(f"{python} -V")
- env = Env(pyver, python, shell)
-
- with change_dir(Path(proj.slug)):
- print(f"Prepping for {proj.slug} {pyver.slug}")
- proj.prep_environment(env)
- for cov_ver in self.cov_versions:
- durations = []
- for run_num in range(num_runs):
- print(
- f"Running tests, cov={cov_ver.slug}, {run_num+1} of {num_runs}"
- )
- if cov_ver.pip_args is None:
- dur = proj.run_no_coverage(env)
- else:
- dur = proj.run_with_coverage(
- env, cov_ver.pip_args, cov_ver.options,
- )
- print(f"Tests took {dur:.3f}s")
- durations.append(dur)
- med = statistics.median(durations)
- result = (
- f"Median for {proj.slug}, {pyver.slug}, "
- + f"cov={cov_ver.slug}: {med:.3f}s"
- )
- print(f"## {result}")
- results.append(result)
- result_key = (proj.slug, pyver.slug, cov_ver.slug)
- self.result_data[result_key] = med
-
- print("# Results")
- for result in results:
- print(result)
-
- def show_results(
- self,
- rows: List[str],
- column: str,
- ratios: Iterable[Tuple[str, str, str]] = (),
- ) -> None:
- dimensions = {
- "cov": [cov_ver.slug for cov_ver in self.cov_versions],
- "pyver": [pyver.slug for pyver in self.py_versions],
- "proj": [proj.slug for proj in self.projects],
- }
-
- table_axes = [dimensions[rowname] for rowname in rows]
- data_order = [*rows, column]
- remap = [data_order.index(datum) for datum in ["proj", "pyver", "cov"]]
-
- WIDTH = 20
- def as_table_row(vals):
- return "| " + " | ".join(v.ljust(WIDTH) for v in vals) + " |"
-
- header = []
- header.extend(rows)
- header.extend(dimensions[column])
- header.extend(slug for slug, _, _ in ratios)
-
- print()
- print(as_table_row(header))
- dashes = [":---"] * len(rows) + ["---:"] * (len(header) - len(rows))
- print(as_table_row(dashes))
- for tup in itertools.product(*table_axes):
- row = []
- row.extend(tup)
- col_data = {}
- for col in dimensions[column]:
- key = (*tup, col)
- key = tuple(key[i] for i in remap)
- result_time = self.result_data[key] # type: ignore
- row.append(f"{result_time:.3f} s")
- col_data[col] = result_time
- for _, num, denom in ratios:
- ratio = col_data[num] / col_data[denom]
- row.append(f"{ratio * 100:.2f}%")
- print(as_table_row(row))
-
-
-PERF_DIR = Path("/tmp/covperf")
-
-def run_experiment(
- py_versions: List[PyVersion], cov_versions: List[Coverage], projects: List[ProjectToTest],
- rows: List[str], column: str, ratios: Iterable[Tuple[str, str, str]] = (),
-):
- slugs = [v.slug for v in py_versions + cov_versions + projects]
- if len(set(slugs)) != len(slugs):
- raise Exception(f"Slugs must be unique: {slugs}")
- if any(" " in slug for slug in slugs):
- raise Exception(f"No spaces in slugs please: {slugs}")
- ratio_slugs = [rslug for ratio in ratios for rslug in ratio[1:]]
- if any(rslug not in slugs for rslug in ratio_slugs):
- raise Exception(f"Ratio slug doesn't match a slug: {ratio_slugs}, {slugs}")
-
- print(f"Removing and re-making {PERF_DIR}")
- rmrf(PERF_DIR)
-
- with change_dir(PERF_DIR):
- exp = Experiment(py_versions=py_versions, cov_versions=cov_versions, projects=projects)
- exp.run(num_runs=int(sys.argv[1]))
- exp.show_results(rows=rows, column=column, ratios=ratios)
-
-
-if 1:
- run_experiment(
- py_versions=[
- #Python(3, 11),
- AdHocPython("/usr/local/cpython/v3.10.5", "v3.10.5"),
- AdHocPython("/usr/local/cpython/v3.11.0b3", "v3.11.0b3"),
- AdHocPython("/usr/local/cpython/94231", "94231"),
- ],
- cov_versions=[
- Coverage("6.4.1", "coverage==6.4.1"),
- ],
- projects=[
- AdHocProject("/src/bugs/bug1339/bug1339.py"),
- SlipcoverBenchmark("bm_sudoku.py"),
- SlipcoverBenchmark("bm_spectral_norm.py"),
- ],
- rows=["cov", "proj"],
- column="pyver",
- ratios=[
- ("3.11b3 vs 3.10", "v3.11.0b3", "v3.10.5"),
- ("94231 vs 3.10", "94231", "v3.10.5"),
- ],
- )
diff -Nru python-coverage-6.5.0+dfsg1/lab/genpy.py python-coverage-7.2.7+dfsg1/lab/genpy.py
--- python-coverage-6.5.0+dfsg1/lab/genpy.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/lab/genpy.py 2023-05-29 19:46:30.000000000 +0000
@@ -231,7 +231,7 @@
source = PythonSpinner.generate_python(maker.make_body("def"))
try:
print("-"*80, "\n", source, sep="")
- compile(source, "", "exec")
+ compile(source, "", "exec", dont_inherit=True)
except Exception as ex:
print(f"Oops: {ex}\n{source}")
if len(source) > len(longest):
diff -Nru python-coverage-6.5.0+dfsg1/lab/parser.py python-coverage-7.2.7+dfsg1/lab/parser.py
--- python-coverage-6.5.0+dfsg1/lab/parser.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/lab/parser.py 2023-05-29 19:46:30.000000000 +0000
@@ -177,7 +177,7 @@
def disassemble(pyparser):
"""Disassemble code, for ad-hoc experimenting."""
- code = compile(pyparser.text, "", "exec")
+ code = compile(pyparser.text, "", "exec", dont_inherit=True)
for code_obj in all_code_objects(code):
if pyparser.text:
srclines = pyparser.text.splitlines()
diff -Nru python-coverage-6.5.0+dfsg1/lab/select_contexts.py python-coverage-7.2.7+dfsg1/lab/select_contexts.py
--- python-coverage-6.5.0+dfsg1/lab/select_contexts.py 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/lab/select_contexts.py 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +1,66 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+"""\
+Select certain contexts from a coverage.py data file.
+"""
+
+import argparse
+import re
+import sys
+
+import coverage
+
+
+def main(argv):
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--include", type=str, help="Regex for contexts to keep")
+ parser.add_argument("--exclude", type=str, help="Regex for contexts to discard")
+ args = parser.parse_args(argv)
+
+ print("** Note: this is a proof-of-concept. Support is not promised. **")
+ print("Feedback is appreciated: https://github.com/nedbat/coveragepy/issues/668")
+
+ cov_in = coverage.Coverage()
+ cov_in.load()
+ data_in = cov_in.get_data()
+ print(f"Contexts in {data_in.data_filename()}:")
+ for ctx in sorted(data_in.measured_contexts()):
+ print(f" {ctx}")
+
+ if args.include is None and args.exclude is None:
+ print("Nothing to do, no output written.")
+ return
+
+ out_file = "output.data"
+ file_names = data_in.measured_files()
+ print(f"{len(file_names)} measured files")
+ print(f"Writing to {out_file}")
+ cov_out = coverage.Coverage(data_file=out_file)
+ data_out = cov_out.get_data()
+
+ for ctx in sorted(data_in.measured_contexts()):
+ if args.include is not None:
+ if not re.search(args.include, ctx):
+ print(f"Skipping context {ctx}, not included")
+ continue
+ if args.exclude is not None:
+ if re.search(args.exclude, ctx):
+ print(f"Skipping context {ctx}, excluded")
+ continue
+ print(f"Keeping context {ctx}")
+ data_in.set_query_context(ctx)
+ data_out.set_context(ctx)
+ if data_in.has_arcs():
+ data_out.add_arcs({f: data_in.arcs(f) for f in file_names})
+ else:
+ data_out.add_lines({f: data_in.lines(f) for f in file_names})
+
+ for fname in file_names:
+ data_out.touch_file(fname, data_in.file_tracer(fname))
+
+ cov_out.save()
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv[1:]))
diff -Nru python-coverage-6.5.0+dfsg1/lab/show_pyc.py python-coverage-7.2.7+dfsg1/lab/show_pyc.py
--- python-coverage-6.5.0+dfsg1/lab/show_pyc.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/lab/show_pyc.py 2023-05-29 19:46:30.000000000 +0000
@@ -48,7 +48,7 @@
show_py_text(text, fname=fname)
def show_py_text(text, fname=""):
- code = compile(text, fname, "exec")
+ code = compile(text, fname, "exec", dont_inherit=True)
show_code(code)
CO_FLAGS = [
diff -Nru python-coverage-6.5.0+dfsg1/Makefile python-coverage-7.2.7+dfsg1/Makefile
--- python-coverage-6.5.0+dfsg1/Makefile 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/Makefile 2023-05-29 19:46:30.000000000 +0000
@@ -11,17 +11,19 @@
clean_platform:
@rm -f *.so */*.so
+ @rm -f *.pyd */*.pyd
@rm -rf __pycache__ */__pycache__ */*/__pycache__ */*/*/__pycache__ */*/*/*/__pycache__ */*/*/*/*/__pycache__
@rm -f *.pyc */*.pyc */*/*.pyc */*/*/*.pyc */*/*/*/*.pyc */*/*/*/*/*.pyc
@rm -f *.pyo */*.pyo */*/*.pyo */*/*/*.pyo */*/*/*/*.pyo */*/*/*/*/*.pyo
+ @rm -f *$$py.class */*$$py.class */*/*$$py.class */*/*/*$$py.class */*/*/*/*$$py.class */*/*/*/*/*$$py.class
clean: clean_platform ## Remove artifacts of test execution, installation, etc.
@echo "Cleaning..."
@-pip uninstall -yq coverage
- @rm -f *.pyd */*.pyd
+ @mkdir -p build # so the chmod won't fail if build doesn't exist
+ @chmod -R 777 build
@rm -rf build coverage.egg-info dist htmlcov
@rm -f *.bak */*.bak */*/*.bak */*/*/*.bak */*/*/*/*.bak */*/*/*/*/*.bak
- @rm -f *$$py.class */*$$py.class */*/*$$py.class */*/*/*$$py.class */*/*/*/*$$py.class */*/*/*/*/*$$py.class
@rm -f coverage/*,cover
@rm -f MANIFEST
@rm -f .coverage .coverage.* coverage.xml coverage.json .metacov*
@@ -30,12 +32,13 @@
@rm -f tests/covmain.zip tests/zipmods.zip tests/zip1.zip
@rm -rf doc/_build doc/_spell doc/sample_html_beta
@rm -rf tmp
- @rm -rf .cache .hypothesis .mypy_cache .pytest_cache
+ @rm -rf .cache .hypothesis .*_cache
@rm -rf tests/actual
@-make -C tests/gold/html clean
sterile: clean ## Remove all non-controlled content, even if expensive.
rm -rf .tox
+ rm -f cheats.txt
help: ## Show this help.
@# Adapted from https://www.thapaliya.com/en/writings/well-documented-makefiles/
@@ -83,7 +86,7 @@
.PHONY: upgrade
-PIP_COMPILE = pip-compile --upgrade --allow-unsafe --generate-hashes
+PIP_COMPILE = pip-compile --upgrade --allow-unsafe --resolver=backtracking
upgrade: export CUSTOM_COMPILE_COMMAND=make upgrade
upgrade: ## Update the *.pip files with the latest packages satisfying *.in files.
pip install -q -r requirements/pip-tools.pip
@@ -96,7 +99,16 @@
$(PIP_COMPILE) -o requirements/light-threads.pip requirements/light-threads.in
$(PIP_COMPILE) -o doc/requirements.pip doc/requirements.in
$(PIP_COMPILE) -o requirements/lint.pip doc/requirements.in requirements/dev.in
+ $(PIP_COMPILE) -o requirements/mypy.pip requirements/mypy.in
+diff_upgrade: ## Summarize the last `make upgrade`
+ @# The sort flags sort by the package name first, then by the -/+, and
+ @# sort by version numbers, so we get a summary with lines like this:
+ @# -bashlex==0.16
+ @# +bashlex==0.17
+ @# -build==0.9.0
+ @# +build==0.10.0
+ @git diff -U0 | grep -v '^@' | grep == | sort -k1.2,1.99 -k1.1,1.1r -u -V
##@ Pre-builds for prepping the code
@@ -122,7 +134,7 @@
_sample_cog_html: clean
python -m pip install -e .
- cd ~/cog/trunk; \
+ cd ~/cog; \
rm -rf htmlcov; \
PYTEST_ADDOPTS= coverage run --branch --source=cogapp -m pytest -k CogTestsInMemory; \
coverage combine; \
@@ -130,12 +142,12 @@
sample_html: _sample_cog_html ## Generate sample HTML report.
rm -f doc/sample_html/*.*
- cp -r ~/cog/trunk/htmlcov/ doc/sample_html/
+ cp -r ~/cog/htmlcov/ doc/sample_html/
rm doc/sample_html/.gitignore
sample_html_beta: _sample_cog_html ## Generate sample HTML report for a beta release.
rm -f doc/sample_html_beta/*.*
- cp -r ~/cog/trunk/htmlcov/ doc/sample_html_beta/
+ cp -r ~/cog/htmlcov/ doc/sample_html_beta/
rm doc/sample_html_beta/.gitignore
@@ -146,6 +158,21 @@
REPO_OWNER = nedbat/coveragepy
+edit_for_release: ## Edit sources to insert release facts.
+ python igor.py edit_for_release
+
+cheats: ## Create some useful snippets for releasing.
+ python igor.py cheats | tee cheats.txt
+
+relbranch: ## Create the branch for releasing.
+ git switch -c nedbat/release-$$(date +%Y%m%d)
+
+relcommit1: ## Commit the first release changes.
+ git commit -am "docs: prep for $$(python setup.py --version)"
+
+relcommit2: ## Commit the latest sample HTML report.
+ git commit -am "docs: sample HTML for $$(python setup.py --version)"
+
kit: ## Make the source distribution.
python -m build
@@ -181,6 +208,12 @@
git branch -f stable $$(python setup.py --version)
git push origin stable
+bump_version: ## Edit sources to bump the version after a release.
+ git switch -c nedbat/bump-version
+ python igor.py bump_version
+ git commit -a -m "build: bump version"
+ git push -u origin @
+
##@ Documentation
@@ -235,8 +268,8 @@
$(RELNOTES_JSON): $(CHANGES_MD)
$(DOCBIN)/python ci/parse_relnotes.py tmp/rst_rst/changes.md $(RELNOTES_JSON)
-github_releases: $(RELNOTES_JSON) ## Update GitHub releases.
- $(DOCBIN)/python ci/github_releases.py $(RELNOTES_JSON) $(REPO_OWNER)
+github_releases: $(DOCBIN) ## Update GitHub releases.
+ $(DOCBIN)/python -m scriv github-release
comment_on_fixes: $(RELNOTES_JSON) ## Add a comment to issues that were fixed.
python ci/comment_on_fixes.py $(REPO_OWNER)
diff -Nru python-coverage-6.5.0+dfsg1/MANIFEST.in python-coverage-7.2.7+dfsg1/MANIFEST.in
--- python-coverage-6.5.0+dfsg1/MANIFEST.in 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/MANIFEST.in 2023-05-29 19:46:30.000000000 +0000
@@ -23,12 +23,14 @@
include setup.py
include tox.ini
include .editorconfig
+include .git-blame-ignore-revs
include .readthedocs.yml
recursive-include ci *
recursive-include lab *
recursive-include .github *
+recursive-include coverage *.pyi
recursive-include coverage/fullcoverage *.py
recursive-include coverage/ctracer *.c *.h
diff -Nru python-coverage-6.5.0+dfsg1/metacov.ini python-coverage-7.2.7+dfsg1/metacov.ini
--- python-coverage-6.5.0+dfsg1/metacov.ini 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/metacov.ini 2023-05-29 19:46:30.000000000 +0000
@@ -8,18 +8,19 @@
[run]
branch = true
-data_file = ${COVERAGE_METAFILE?}
+data_file = ${COVERAGE_METAFILE-.metacov}
parallel = true
+relative_files = true
source =
${COVERAGE_HOME-.}/coverage
${COVERAGE_HOME-.}/tests
# $set_env.py: COVERAGE_DYNCTX - Set to 'test_function' for who-tests-what
dynamic_context = ${COVERAGE_DYNCTX-none}
-# $set_env.py: COVERAGE_CONTEXT - Set to a static context for this run
+# $set_env.py: COVERAGE_CONTEXT - Static context for this run (or $ENV_VAR like $TOX_ENV_NAME)
context = ${COVERAGE_CONTEXT-none}
[report]
-# We set a different pragmas so our code won't be confused with test code, and
+# We set different pragmas so our code won't be confused with test code, and
# we use different pragmas for different reasons that the lines won't be
# measured.
exclude_lines =
@@ -58,6 +59,10 @@
raise AssertionError
pragma: only failure
+ # Not-real code for type checking
+ if TYPE_CHECKING:
+ class .*\(Protocol\):
+
# OS error conditions that we can't (or don't care to) replicate.
pragma: cant happen
@@ -65,23 +70,15 @@
# longer tested.
pragma: obscure
- # Jython needs special care.
- pragma: only jython
- if env.JYTHON
-
- # IronPython isn't included in metacoverage.
- pragma: only ironpython
- if env.IRONPYTHON
-
partial_branches =
pragma: part covered
# A for-loop that always hits its break statement
pragma: always breaks
pragma: part started
+ # If we're asserting that any() is true, it didn't finish.
+ assert any\(
if env.TESTING:
if env.METACOV:
- if .* env.JYTHON
- if .* env.IRONPYTHON
precision = 3
@@ -91,11 +88,9 @@
[paths]
source =
.
- *\coverage\trunk
*/coverage/trunk
- *\coveragepy
- /io
# GitHub Actions on Ubuntu uses /home/runner/work/coveragepy
# GitHub Actions on Mac uses /Users/runner/work/coveragepy
# GitHub Actions on Window uses D:\a\coveragepy\coveragepy
+ *\coveragepy
*/coveragepy
diff -Nru python-coverage-6.5.0+dfsg1/NOTICE.txt python-coverage-7.2.7+dfsg1/NOTICE.txt
--- python-coverage-6.5.0+dfsg1/NOTICE.txt 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/NOTICE.txt 2023-05-29 19:46:30.000000000 +0000
@@ -1,5 +1,5 @@
Copyright 2001 Gareth Rees. All rights reserved.
-Copyright 2004-2022 Ned Batchelder. All rights reserved.
+Copyright 2004-2023 Ned Batchelder. All rights reserved.
Except where noted otherwise, this software is licensed under the Apache
License, Version 2.0 (the "License"); you may not use this work except in
diff -Nru python-coverage-6.5.0+dfsg1/PKG-INFO python-coverage-7.2.7+dfsg1/PKG-INFO
--- python-coverage-6.5.0+dfsg1/PKG-INFO 2022-09-29 16:36:48.202740000 +0000
+++ python-coverage-7.2.7+dfsg1/PKG-INFO 2023-05-29 19:46:41.742986000 +0000
@@ -1,15 +1,16 @@
Metadata-Version: 2.1
Name: coverage
-Version: 6.5.0
+Version: 7.2.7
Summary: Code coverage measurement for Python
Home-page: https://github.com/nedbat/coveragepy
-Author: Ned Batchelder and 161 others
+Author: Ned Batchelder and 213 others
Author-email: ned@nedbatchelder.com
-License: Apache 2.0
-Project-URL: Documentation, https://coverage.readthedocs.io
+License: Apache-2.0
+Project-URL: Documentation, https://coverage.readthedocs.io/en/7.2.7
Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=pypi
Project-URL: Issues, https://github.com/nedbat/coveragepy/issues
-Project-URL: Twitter, https://twitter.com/coveragepy
+Project-URL: Mastodon, https://hachyderm.io/@coveragepy
+Project-URL: Mastodon (nedbat), https://hachyderm.io/@nedbat
Keywords: code coverage testing
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
@@ -22,6 +23,7 @@
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Software Development :: Quality Assurance
@@ -51,8 +53,8 @@
| |test-status| |quality-status| |docs| |metacov|
| |kit| |downloads| |format| |repos|
| |stars| |forks| |contributors|
-| |tidelift| |core-infrastructure| |open-ssf|
-| |sponsor| |twitter-coveragepy| |twitter-nedbat|
+| |core-infrastructure| |open-ssf| |snyk|
+| |tidelift| |sponsor| |mastodon-coveragepy| |mastodon-nedbat|
Coverage.py measures code coverage, typically during test execution. It uses
the code analysis tools and tracing hooks provided in the Python standard
@@ -62,17 +64,23 @@
.. PYVERSIONS
-* CPython 3.7 through 3.11.0 rc2.
-* PyPy3 7.3.8.
+* CPython 3.7 through 3.12.0b1
+* PyPy3 7.3.11.
Documentation is on `Read the Docs`_. Code repository and issue tracker are on
`GitHub`_.
-.. _Read the Docs: https://coverage.readthedocs.io/
+.. _Read the Docs: https://coverage.readthedocs.io/en/7.2.7/
.. _GitHub: https://github.com/nedbat/coveragepy
+**New in 7.x:**
+improved data combining;
+``[run] exclude_also`` setting;
+``report --format=``;
+type annotations.
-**New in 6.x:** dropped support for Python 2.7, 3.5, and 3.6;
+**New in 6.x:**
+dropped support for Python 2.7, 3.5, and 3.6;
write data on SIGTERM;
added support for 3.10 match/case statements.
@@ -99,9 +107,10 @@
Getting Started
---------------
-See the `Quick Start section`_ of the docs.
+Looking to run ``coverage`` on your test suite? See the `Quick Start section`_
+of the docs.
-.. _Quick Start section: https://coverage.readthedocs.io/#quick-start
+.. _Quick Start section: https://coverage.readthedocs.io/en/7.2.7/#quick-start
Change history
@@ -109,7 +118,7 @@
The complete history of changes is on the `change history page`_.
-.. _change history page: https://coverage.readthedocs.io/en/latest/changes.html
+.. _change history page: https://coverage.readthedocs.io/en/7.2.7/changes.html
Code of Conduct
@@ -125,9 +134,10 @@
Contributing
------------
-See the `Contributing section`_ of the docs.
+Found a bug? Want to help improve the code or documentation? See the
+`Contributing section`_ of the docs.
-.. _Contributing section: https://coverage.readthedocs.io/en/latest/contributing.html
+.. _Contributing section: https://coverage.readthedocs.io/en/7.2.7/contributing.html
Security
@@ -155,7 +165,7 @@
:target: https://github.com/nedbat/coveragepy/actions/workflows/quality.yml
:alt: Quality check status
.. |docs| image:: https://readthedocs.org/projects/coverage/badge/?version=latest&style=flat
- :target: https://coverage.readthedocs.io/
+ :target: https://coverage.readthedocs.io/en/7.2.7/
:alt: Documentation
.. |kit| image:: https://badge.fury.io/py/coverage.svg
:target: https://pypi.org/project/coverage/
@@ -193,12 +203,12 @@
.. |contributors| image:: https://img.shields.io/github/contributors/nedbat/coveragepy.svg?logo=github
:target: https://github.com/nedbat/coveragepy/graphs/contributors
:alt: Contributors
-.. |twitter-coveragepy| image:: https://img.shields.io/twitter/follow/coveragepy.svg?label=coveragepy&style=flat&logo=twitter&logoColor=4FADFF
- :target: https://twitter.com/coveragepy
- :alt: coverage.py on Twitter
-.. |twitter-nedbat| image:: https://img.shields.io/twitter/follow/nedbat.svg?label=nedbat&style=flat&logo=twitter&logoColor=4FADFF
- :target: https://twitter.com/nedbat
- :alt: nedbat on Twitter
+.. |mastodon-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&link=https%3A%2F%2Fhachyderm.io%2F%40nedbat&url=https%3A%2F%2Fhachyderm.io%2Fusers%2Fnedbat%2Ffollowers.json&query=totalItems&label=@nedbat
+ :target: https://hachyderm.io/@nedbat
+ :alt: nedbat on Mastodon
+.. |mastodon-coveragepy| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&link=https%3A%2F%2Fhachyderm.io%2F%40coveragepy&url=https%3A%2F%2Fhachyderm.io%2Fusers%2Fcoveragepy%2Ffollowers.json&query=totalItems&label=@coveragepy
+ :target: https://hachyderm.io/@coveragepy
+ :alt: coveragepy on Mastodon
.. |sponsor| image:: https://img.shields.io/badge/%E2%9D%A4-Sponsor%20me-brightgreen?style=flat&logo=GitHub
:target: https://github.com/sponsors/nedbat
:alt: Sponsor me on GitHub
@@ -208,3 +218,6 @@
.. |open-ssf| image:: https://api.securityscorecards.dev/projects/github.com/nedbat/coveragepy/badge
:target: https://deps.dev/pypi/coverage
:alt: OpenSSF Scorecard
+.. |snyk| image:: https://snyk.io/advisor/python/coverage/badge.svg
+ :target: https://snyk.io/advisor/python/coverage
+ :alt: Snyk package health
diff -Nru python-coverage-6.5.0+dfsg1/pylintrc python-coverage-7.2.7+dfsg1/pylintrc
--- python-coverage-6.5.0+dfsg1/pylintrc 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/pylintrc 2023-05-29 19:46:30.000000000 +0000
@@ -62,6 +62,7 @@
broad-except,
no-else-return,
subprocess-run-check,
+ use-dict-literal,
# Messages that may be silly:
no-member,
using-constant-test,
@@ -75,6 +76,7 @@
self-assigning-variable,
consider-using-with,
missing-timeout,
+ use-implicit-booleaness-not-comparison,
# Formatting stuff
superfluous-parens,
# Messages that are noisy for now, eventually maybe we'll turn them on:
diff -Nru python-coverage-6.5.0+dfsg1/pyproject.toml python-coverage-7.2.7+dfsg1/pyproject.toml
--- python-coverage-6.5.0+dfsg1/pyproject.toml 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/pyproject.toml 2023-05-29 19:46:30.000000000 +0000
@@ -2,5 +2,65 @@
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
[build-system]
-requires = ['setuptools', 'wheel']
+requires = ['setuptools']
build-backend = 'setuptools.build_meta'
+
+[tool.mypy]
+check_untyped_defs = true
+disallow_any_generics = true
+disallow_incomplete_defs = true
+disallow_subclassing_any = true
+disallow_untyped_calls = true
+disallow_untyped_decorators = true
+disallow_untyped_defs = true
+follow_imports = "silent"
+ignore_missing_imports = true
+no_implicit_optional = true
+show_error_codes = true
+warn_redundant_casts = true
+warn_return_any = true
+warn_unreachable = true
+warn_unused_configs = true
+warn_unused_ignores = true
+
+exclude = """(?x)(
+ ^coverage/fullcoverage/encodings\\.py$ # can't import things into it.
+ | ^tests/balance_xdist_plugin\\.py$ # not part of our test suite.
+ )"""
+
+[tool.pytest.ini_options]
+addopts = "-q -n auto -p no:legacypath --strict-markers --no-flaky-report -rfEX --failed-first"
+python_classes = "*Test"
+markers = [
+ "expensive: too slow to run during \"make smoke\"",
+]
+
+# How come these warnings are suppressed successfully here, but not in conftest.py??
+filterwarnings = [
+ "ignore:the imp module is deprecated in favour of importlib:DeprecationWarning",
+ "ignore:distutils Version classes are deprecated:DeprecationWarning",
+ "ignore:The distutils package is deprecated and slated for removal in Python 3.12:DeprecationWarning",
+]
+
+# xfail tests that pass should fail the test suite
+xfail_strict = true
+
+balanced_clumps = [
+ # Because of expensive session-scoped fixture:
+ "VirtualenvTest",
+ # Because of shared-file manipulations (~/tests/actual/testing):
+ "CompareTest",
+ # No idea why this one fails if run on separate workers:
+ "GetZipBytesTest",
+]
+
+[tool.ruff]
+line-length = 100
+
+[tool.scriv]
+# Changelog management: https://pypi.org/project/scriv/
+format = "rst"
+output_file = "CHANGES.rst"
+insert_marker = "scriv-start-here"
+end_marker = "scriv-end-here"
+ghrel_template = "file: ci/ghrel_template.md.j2"
diff -Nru python-coverage-6.5.0+dfsg1/README.rst python-coverage-7.2.7+dfsg1/README.rst
--- python-coverage-6.5.0+dfsg1/README.rst 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/README.rst 2023-05-29 19:46:30.000000000 +0000
@@ -17,8 +17,8 @@
| |test-status| |quality-status| |docs| |metacov|
| |kit| |downloads| |format| |repos|
| |stars| |forks| |contributors|
-| |tidelift| |core-infrastructure| |open-ssf|
-| |sponsor| |twitter-coveragepy| |twitter-nedbat|
+| |core-infrastructure| |open-ssf| |snyk|
+| |tidelift| |sponsor| |mastodon-coveragepy| |mastodon-nedbat|
Coverage.py measures code coverage, typically during test execution. It uses
the code analysis tools and tracing hooks provided in the Python standard
@@ -28,8 +28,8 @@
.. PYVERSIONS
-* CPython 3.7 through 3.11.0 rc2.
-* PyPy3 7.3.8.
+* CPython 3.7 through 3.12.0b1
+* PyPy3 7.3.11.
Documentation is on `Read the Docs`_. Code repository and issue tracker are on
`GitHub`_.
@@ -37,8 +37,14 @@
.. _Read the Docs: https://coverage.readthedocs.io/
.. _GitHub: https://github.com/nedbat/coveragepy
+**New in 7.x:**
+improved data combining;
+``[run] exclude_also`` setting;
+``report --format=``;
+type annotations.
-**New in 6.x:** dropped support for Python 2.7, 3.5, and 3.6;
+**New in 6.x:**
+dropped support for Python 2.7, 3.5, and 3.6;
write data on SIGTERM;
added support for 3.10 match/case statements.
@@ -65,7 +71,8 @@
Getting Started
---------------
-See the `Quick Start section`_ of the docs.
+Looking to run ``coverage`` on your test suite? See the `Quick Start section`_
+of the docs.
.. _Quick Start section: https://coverage.readthedocs.io/#quick-start
@@ -91,7 +98,8 @@
Contributing
------------
-See the `Contributing section`_ of the docs.
+Found a bug? Want to help improve the code or documentation? See the
+`Contributing section`_ of the docs.
.. _Contributing section: https://coverage.readthedocs.io/en/latest/contributing.html
@@ -159,12 +167,12 @@
.. |contributors| image:: https://img.shields.io/github/contributors/nedbat/coveragepy.svg?logo=github
:target: https://github.com/nedbat/coveragepy/graphs/contributors
:alt: Contributors
-.. |twitter-coveragepy| image:: https://img.shields.io/twitter/follow/coveragepy.svg?label=coveragepy&style=flat&logo=twitter&logoColor=4FADFF
- :target: https://twitter.com/coveragepy
- :alt: coverage.py on Twitter
-.. |twitter-nedbat| image:: https://img.shields.io/twitter/follow/nedbat.svg?label=nedbat&style=flat&logo=twitter&logoColor=4FADFF
- :target: https://twitter.com/nedbat
- :alt: nedbat on Twitter
+.. |mastodon-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&link=https%3A%2F%2Fhachyderm.io%2F%40nedbat&url=https%3A%2F%2Fhachyderm.io%2Fusers%2Fnedbat%2Ffollowers.json&query=totalItems&label=@nedbat
+ :target: https://hachyderm.io/@nedbat
+ :alt: nedbat on Mastodon
+.. |mastodon-coveragepy| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&link=https%3A%2F%2Fhachyderm.io%2F%40coveragepy&url=https%3A%2F%2Fhachyderm.io%2Fusers%2Fcoveragepy%2Ffollowers.json&query=totalItems&label=@coveragepy
+ :target: https://hachyderm.io/@coveragepy
+ :alt: coveragepy on Mastodon
.. |sponsor| image:: https://img.shields.io/badge/%E2%9D%A4-Sponsor%20me-brightgreen?style=flat&logo=GitHub
:target: https://github.com/sponsors/nedbat
:alt: Sponsor me on GitHub
@@ -174,3 +182,6 @@
.. |open-ssf| image:: https://api.securityscorecards.dev/projects/github.com/nedbat/coveragepy/badge
:target: https://deps.dev/pypi/coverage
:alt: OpenSSF Scorecard
+.. |snyk| image:: https://snyk.io/advisor/python/coverage/badge.svg
+ :target: https://snyk.io/advisor/python/coverage
+ :alt: Snyk package health
diff -Nru python-coverage-6.5.0+dfsg1/.readthedocs.yml python-coverage-7.2.7+dfsg1/.readthedocs.yml
--- python-coverage-6.5.0+dfsg1/.readthedocs.yml 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/.readthedocs.yml 2023-05-29 19:46:30.000000000 +0000
@@ -17,6 +17,7 @@
- pdf
python:
+ # PYVERSIONS
version: 3.7
install:
- requirements: doc/requirements.pip
diff -Nru python-coverage-6.5.0+dfsg1/requirements/dev.in python-coverage-7.2.7+dfsg1/requirements/dev.in
--- python-coverage-6.5.0+dfsg1/requirements/dev.in 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/requirements/dev.in 2023-05-29 19:46:30.000000000 +0000
@@ -5,17 +5,17 @@
# "make upgrade" turns this into requirements/dev.pip.
-c pins.pip
--r pip.pip
+-r pip.in
# PyPI requirements for running tests.
-tox
--r pytest.pip
+-r tox.in
+-r pytest.in
# for linting.
+check-manifest
cogapp
greenlet
pylint
-check-manifest
readme_renderer
# for kitting.
diff -Nru python-coverage-6.5.0+dfsg1/requirements/dev.pip python-coverage-7.2.7+dfsg1/requirements/dev.pip
--- python-coverage-6.5.0+dfsg1/requirements/dev.pip 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/requirements/dev.pip 2023-05-29 19:46:30.000000000 +0000
@@ -1,565 +1,207 @@
#
-# This file is autogenerated by pip-compile with python 3.7
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.7
+# by the following command:
#
# make upgrade
#
-astroid==2.12.10 \
- --hash=sha256:81f870105d892e73bf535da77a8261aa5bde838fa4ed12bb2f435291a098c581 \
- --hash=sha256:997e0c735df60d4a4caff27080a3afc51f9bdd693d3572a4a0b7090b645c36c5
+astroid==2.15.5
# via pylint
-atomicwrites==1.4.1 \
- --hash=sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11
- # via -r requirements/pytest.pip
-attrs==22.1.0 \
- --hash=sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6 \
- --hash=sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c
- # via
- # -r requirements/pytest.pip
- # hypothesis
- # pytest
-backports-functools-lru-cache==1.6.4 \
- --hash=sha256:d5ed2169378b67d3c545e5600d363a923b09c456dab1593914935a68ad478271 \
- --hash=sha256:dbead04b9daa817909ec64e8d2855fb78feafe0b901d4568758e3a60559d8978
- # via
- # -r requirements/pytest.pip
- # pycontracts
-bleach==5.0.1 \
- --hash=sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a \
- --hash=sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c
+attrs==23.1.0
+ # via hypothesis
+bleach==6.0.0
# via readme-renderer
-build==0.8.0 \
- --hash=sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437 \
- --hash=sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0
+build==0.10.0
# via check-manifest
-certifi==2022.5.18.1 \
- --hash=sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7 \
- --hash=sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a
- # via
- # -c requirements/pins.pip
- # requests
-charset-normalizer==2.1.1 \
- --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \
- --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f
+cachetools==5.3.1
+ # via tox
+certifi==2023.5.7
# via requests
-check-manifest==0.48 \
- --hash=sha256:3b575f1dade7beb3078ef4bf33a94519834457c7281dbc726b15c5466b55c657 \
- --hash=sha256:b1923685f98c1c2468601a1a7bed655db549a25d43c583caded3860ad8308f8c
+chardet==5.1.0
+ # via tox
+charset-normalizer==3.1.0
+ # via requests
+check-manifest==0.49
# via -r requirements/dev.in
-cogapp==3.3.0 \
- --hash=sha256:1be95183f70282422d594fa42426be6923070a4bd8335621f6347f3aeee81db0 \
- --hash=sha256:8b5b5f6063d8ee231961c05da010cb27c30876b2279e23ad0eae5f8f09460d50
+cogapp==3.3.0
# via -r requirements/dev.in
-colorama==0.4.5 \
- --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \
- --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4
- # via -r requirements/pytest.pip
-commonmark==0.9.1 \
- --hash=sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60 \
- --hash=sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9
- # via rich
-decorator==5.1.1 \
- --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \
- --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186
- # via
- # -r requirements/pytest.pip
- # pycontracts
-dill==0.3.5.1 \
- --hash=sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302 \
- --hash=sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86
- # via pylint
-distlib==0.3.6 \
- --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \
- --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e
+colorama==0.4.6
# via
- # -r requirements/pip.pip
- # virtualenv
-docutils==0.19 \
- --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \
- --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc
+ # -r requirements/pytest.in
+ # -r requirements/tox.in
+ # tox
+dill==0.3.6
+ # via pylint
+distlib==0.3.6
+ # via virtualenv
+docutils==0.20.1
# via readme-renderer
-exceptiongroup==1.0.0rc9 \
- --hash=sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337 \
- --hash=sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96
+exceptiongroup==1.1.1
# via
- # -r requirements/pytest.pip
# hypothesis
-execnet==1.9.0 \
- --hash=sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5 \
- --hash=sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142
- # via
- # -r requirements/pytest.pip
- # pytest-xdist
-filelock==3.8.0 \
- --hash=sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc \
- --hash=sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4
+ # pytest
+execnet==1.9.0
+ # via pytest-xdist
+filelock==3.12.0
# via
- # -r requirements/pip.pip
# tox
# virtualenv
-flaky==3.7.0 \
- --hash=sha256:3ad100780721a1911f57a165809b7ea265a7863305acb66708220820caf8aa0d \
- --hash=sha256:d6eda73cab5ae7364504b7c44670f70abed9e75f77dd116352f662817592ec9c
- # via -r requirements/pytest.pip
-future==0.18.2 \
- --hash=sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d
- # via
- # -r requirements/pytest.pip
- # pycontracts
-greenlet==1.1.3 \
- --hash=sha256:0118817c9341ef2b0f75f5af79ac377e4da6ff637e5ee4ac91802c0e379dadb4 \
- --hash=sha256:048d2bed76c2aa6de7af500ae0ea51dd2267aec0e0f2a436981159053d0bc7cc \
- --hash=sha256:07c58e169bbe1e87b8bbf15a5c1b779a7616df9fd3e61cadc9d691740015b4f8 \
- --hash=sha256:095a980288fe05adf3d002fbb180c99bdcf0f930e220aa66fcd56e7914a38202 \
- --hash=sha256:0b181e9aa6cb2f5ec0cacc8cee6e5a3093416c841ba32c185c30c160487f0380 \
- --hash=sha256:1626185d938d7381631e48e6f7713e8d4b964be246073e1a1d15c2f061ac9f08 \
- --hash=sha256:184416e481295832350a4bf731ba619a92f5689bf5d0fa4341e98b98b1265bd7 \
- --hash=sha256:1dd51d2650e70c6c4af37f454737bf4a11e568945b27f74b471e8e2a9fd21268 \
- --hash=sha256:1ec2779774d8e42ed0440cf8bc55540175187e8e934f2be25199bf4ed948cd9e \
- --hash=sha256:2cf45e339cabea16c07586306a31cfcc5a3b5e1626d365714d283732afed6809 \
- --hash=sha256:2fb0aa7f6996879551fd67461d5d3ab0c3c0245da98be90c89fcb7a18d437403 \
- --hash=sha256:44b4817c34c9272c65550b788913620f1fdc80362b209bc9d7dd2f40d8793080 \
- --hash=sha256:466ce0928e33421ee84ae04c4ac6f253a3a3e6b8d600a79bd43fd4403e0a7a76 \
- --hash=sha256:4f166b4aca8d7d489e82d74627a7069ab34211ef5ebb57c300ec4b9337b60fc0 \
- --hash=sha256:510c3b15587afce9800198b4b142202b323bf4b4b5f9d6c79cb9a35e5e3c30d2 \
- --hash=sha256:5b756e6730ea59b2745072e28ad27f4c837084688e6a6b3633c8b1e509e6ae0e \
- --hash=sha256:5fbe1ab72b998ca77ceabbae63a9b2e2dc2d963f4299b9b278252ddba142d3f1 \
- --hash=sha256:6200a11f003ec26815f7e3d2ded01b43a3810be3528dd760d2f1fa777490c3cd \
- --hash=sha256:65ad1a7a463a2a6f863661329a944a5802c7129f7ad33583dcc11069c17e622c \
- --hash=sha256:694ffa7144fa5cc526c8f4512665003a39fa09ef00d19bbca5c8d3406db72fbe \
- --hash=sha256:6f5d4b2280ceea76c55c893827961ed0a6eadd5a584a7c4e6e6dd7bc10dfdd96 \
- --hash=sha256:7532a46505470be30cbf1dbadb20379fb481244f1ca54207d7df3bf0bbab6a20 \
- --hash=sha256:76a53bfa10b367ee734b95988bd82a9a5f0038a25030f9f23bbbc005010ca600 \
- --hash=sha256:77e41db75f9958f2083e03e9dd39da12247b3430c92267df3af77c83d8ff9eed \
- --hash=sha256:7a43bbfa9b6cfdfaeefbd91038dde65ea2c421dc387ed171613df340650874f2 \
- --hash=sha256:7b41d19c0cfe5c259fe6c539fd75051cd39a5d33d05482f885faf43f7f5e7d26 \
- --hash=sha256:7c5227963409551ae4a6938beb70d56bf1918c554a287d3da6853526212fbe0a \
- --hash=sha256:870a48007872d12e95a996fca3c03a64290d3ea2e61076aa35d3b253cf34cd32 \
- --hash=sha256:88b04e12c9b041a1e0bcb886fec709c488192638a9a7a3677513ac6ba81d8e79 \
- --hash=sha256:8c287ae7ac921dfde88b1c125bd9590b7ec3c900c2d3db5197f1286e144e712b \
- --hash=sha256:903fa5716b8fbb21019268b44f73f3748c41d1a30d71b4a49c84b642c2fed5fa \
- --hash=sha256:9537e4baf0db67f382eb29255a03154fcd4984638303ff9baaa738b10371fa57 \
- --hash=sha256:9951dcbd37850da32b2cb6e391f621c1ee456191c6ae5528af4a34afe357c30e \
- --hash=sha256:9b2f7d0408ddeb8ea1fd43d3db79a8cefaccadd2a812f021333b338ed6b10aba \
- --hash=sha256:9c88e134d51d5e82315a7c32b914a58751b7353eb5268dbd02eabf020b4c4700 \
- --hash=sha256:9fae214f6c43cd47f7bef98c56919b9222481e833be2915f6857a1e9e8a15318 \
- --hash=sha256:a3a669f11289a8995d24fbfc0e63f8289dd03c9aaa0cc8f1eab31d18ca61a382 \
- --hash=sha256:aa741c1a8a8cc25eb3a3a01a62bdb5095a773d8c6a86470bde7f607a447e7905 \
- --hash=sha256:b0877a9a2129a2c56a2eae2da016743db7d9d6a05d5e1c198f1b7808c602a30e \
- --hash=sha256:bcb6c6dd1d6be6d38d6db283747d07fda089ff8c559a835236560a4410340455 \
- --hash=sha256:caff52cb5cd7626872d9696aee5b794abe172804beb7db52eed1fd5824b63910 \
- --hash=sha256:cbc1eb55342cbac8f7ec159088d54e2cfdd5ddf61c87b8bbe682d113789331b2 \
- --hash=sha256:cd16a89efe3a003029c87ff19e9fba635864e064da646bc749fc1908a4af18f3 \
- --hash=sha256:ce5b64dfe8d0cca407d88b0ee619d80d4215a2612c1af8c98a92180e7109f4b5 \
- --hash=sha256:d58a5a71c4c37354f9e0c24c9c8321f0185f6945ef027460b809f4bb474bfe41 \
- --hash=sha256:db41f3845eb579b544c962864cce2c2a0257fe30f0f1e18e51b1e8cbb4e0ac6d \
- --hash=sha256:db5b25265010a1b3dca6a174a443a0ed4c4ab12d5e2883a11c97d6e6d59b12f9 \
- --hash=sha256:dd0404d154084a371e6d2bafc787201612a1359c2dee688ae334f9118aa0bf47 \
- --hash=sha256:de431765bd5fe62119e0bc6bc6e7b17ac53017ae1782acf88fcf6b7eae475a49 \
- --hash=sha256:df02fdec0c533301497acb0bc0f27f479a3a63dcdc3a099ae33a902857f07477 \
- --hash=sha256:e8533f5111704d75de3139bf0b8136d3a6c1642c55c067866fa0a51c2155ee33 \
- --hash=sha256:f2f908239b7098799b8845e5936c2ccb91d8c2323be02e82f8dcb4a80dcf4a25 \
- --hash=sha256:f8bfd36f368efe0ab2a6aa3db7f14598aac454b06849fb633b762ddbede1db90 \
- --hash=sha256:ffe73f9e7aea404722058405ff24041e59d31ca23d1da0895af48050a07b6932
- # via -r requirements/dev.in
-hypothesis==6.54.6 \
- --hash=sha256:2d5e2d5ccd0efce4e0968a6164f4e4853f808e33f4d91490c975c98beec0c7c3 \
- --hash=sha256:e44833325f9a55f795596ceefd7ede7d626cfe45836025d2647cccaff7070e10
- # via -r requirements/pytest.pip
-idna==3.4 \
- --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
- --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+flaky==3.7.0
+ # via -r requirements/pytest.in
+greenlet==2.0.2
+ # via -r requirements/dev.in
+hypothesis==6.75.6
+ # via -r requirements/pytest.in
+idna==3.4
# via requests
-importlib-metadata==4.12.0 \
- --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \
- --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23
+importlib-metadata==6.6.0
# via
- # -r requirements/pip.pip
- # -r requirements/pytest.pip
+ # attrs
# build
# keyring
- # pep517
# pluggy
# pytest
# tox
# twine
# virtualenv
-iniconfig==1.1.1 \
- --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \
- --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32
- # via
- # -r requirements/pytest.pip
- # pytest
-isort==5.10.1 \
- --hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
- --hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
+importlib-resources==5.12.0
+ # via keyring
+iniconfig==2.0.0
+ # via pytest
+isort==5.11.5
# via pylint
-jaraco-classes==3.2.3 \
- --hash=sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158 \
- --hash=sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a
+jaraco-classes==3.2.3
# via keyring
-jedi==0.18.1 \
- --hash=sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d \
- --hash=sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab
+jedi==0.18.2
# via pudb
-keyring==23.9.3 \
- --hash=sha256:69732a15cb1433bdfbc3b980a8a36a04878a6cfd7cb99f497b573f31618001c0 \
- --hash=sha256:69b01dd83c42f590250fe7a1f503fc229b14de83857314b1933a3ddbf595c4a5
+keyring==23.13.1
# via twine
-lazy-object-proxy==1.7.1 \
- --hash=sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7 \
- --hash=sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a \
- --hash=sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c \
- --hash=sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc \
- --hash=sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f \
- --hash=sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09 \
- --hash=sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442 \
- --hash=sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e \
- --hash=sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029 \
- --hash=sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61 \
- --hash=sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb \
- --hash=sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0 \
- --hash=sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35 \
- --hash=sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42 \
- --hash=sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1 \
- --hash=sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad \
- --hash=sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443 \
- --hash=sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd \
- --hash=sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9 \
- --hash=sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148 \
- --hash=sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38 \
- --hash=sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55 \
- --hash=sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36 \
- --hash=sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a \
- --hash=sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b \
- --hash=sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44 \
- --hash=sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6 \
- --hash=sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69 \
- --hash=sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4 \
- --hash=sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84 \
- --hash=sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de \
- --hash=sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28 \
- --hash=sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c \
- --hash=sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1 \
- --hash=sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8 \
- --hash=sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b \
- --hash=sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb
+lazy-object-proxy==1.9.0
# via astroid
-libsass==0.21.0 \
- --hash=sha256:06c8776417fe930714bdc930a3d7e795ae3d72be6ac883ff72a1b8f7c49e5ffb \
- --hash=sha256:12f39712de38689a8b785b7db41d3ba2ea1d46f9379d81ea4595802d91fa6529 \
- --hash=sha256:1e25dd9047a9392d3c59a0b869e0404f2b325a03871ee45285ee33b3664f5613 \
- --hash=sha256:659ae41af8708681fa3ec73f47b9735a6725e71c3b66ff570bfce78952f2314e \
- --hash=sha256:6b984510ed94993708c0d697b4fef2d118929bbfffc3b90037be0f5ccadf55e7 \
- --hash=sha256:a005f298f64624f313a3ac618ab03f844c71d84ae4f4a4aec4b68d2a4ffe75eb \
- --hash=sha256:abc29357ee540849faf1383e1746d40d69ed5cb6d4c346df276b258f5aa8977a \
- --hash=sha256:c9ec490609752c1d81ff6290da33485aa7cb6d7365ac665b74464c1b7d97f7da \
- --hash=sha256:d5ba529d9ce668be9380563279f3ffe988f27bc5b299c5a28453df2e0b0fbaf2 \
- --hash=sha256:e2b1a7d093f2e76dc694c17c0c285e846d0b0deb0e8b21dc852ba1a3a4e2f1d6
+libsass==0.22.0
# via -r requirements/dev.in
-mccabe==0.7.0 \
- --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
- --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
+markdown-it-py==2.2.0
+ # via rich
+mccabe==0.7.0
# via pylint
-more-itertools==8.14.0 \
- --hash=sha256:1bc4f91ee5b1b31ac7ceacc17c09befe6a40a503907baf9c839c229b5095cfd2 \
- --hash=sha256:c09443cd3d5438b8dafccd867a6bc1cb0894389e90cb53d227456b0b0bccb750
+mdurl==0.1.2
+ # via markdown-it-py
+more-itertools==9.1.0
# via jaraco-classes
-packaging==21.3 \
- --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
- --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
+packaging==23.1
# via
- # -r requirements/pytest.pip
# build
# pudb
+ # pyproject-api
# pytest
# tox
-parso==0.8.3 \
- --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \
- --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75
+parso==0.8.3
# via jedi
-pep517==0.13.0 \
- --hash=sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b \
- --hash=sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59
- # via build
-pkginfo==1.8.3 \
- --hash=sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594 \
- --hash=sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c
+pkginfo==1.9.6
# via twine
-platformdirs==2.5.2 \
- --hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
- --hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19
+platformdirs==3.5.1
# via
- # -r requirements/pip.pip
# pylint
+ # tox
# virtualenv
-pluggy==1.0.0 \
- --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \
- --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3
+pluggy==1.0.0
# via
- # -r requirements/pytest.pip
# pytest
# tox
-pudb==2022.1.2 \
- --hash=sha256:6b83ab805bddb53710109690a2237e98bf83c0b3a00033c517cdf5f6a8fa470d
+pudb==2022.1.3
# via -r requirements/dev.in
-py==1.11.0 \
- --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \
- --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378
- # via
- # -r requirements/pytest.pip
- # pytest
- # pytest-forked
- # tox
-pycontracts @ https://github.com/slorg1/contracts/archive/c5a6da27d4dc9985f68e574d20d86000880919c3.zip \
- --hash=sha256:2b889cbfb03b43dc811b5879248ac5c7e209ece78f03be9633de76a6b21a5a89
- # via -r requirements/pytest.pip
-pygments==2.13.0 \
- --hash=sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1 \
- --hash=sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42
+pygments==2.15.1
# via
# pudb
# readme-renderer
# rich
-pylint==2.15.3 \
- --hash=sha256:5fdfd44af182866999e6123139d265334267339f29961f00c89783155eacc60b \
- --hash=sha256:7f6aad1d8d50807f7bc64f89ac75256a9baf8e6ed491cc9bc65592bc3f462cf1
+pylint==2.17.4
# via -r requirements/dev.in
-pyparsing==3.0.9 \
- --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
- --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
- # via
- # -r requirements/pytest.pip
- # packaging
- # pycontracts
-pytest==7.1.3 \
- --hash=sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7 \
- --hash=sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39
- # via
- # -r requirements/pytest.pip
- # pytest-forked
- # pytest-xdist
-pytest-forked==1.4.0 \
- --hash=sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e \
- --hash=sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8
+pyproject-api==1.5.1
+ # via tox
+pyproject-hooks==1.0.0
+ # via build
+pytest==7.3.1
# via
- # -r requirements/pytest.pip
+ # -r requirements/pytest.in
# pytest-xdist
-pytest-xdist==2.5.0 \
- --hash=sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf \
- --hash=sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65
- # via -r requirements/pytest.pip
-qualname==0.1.0 \
- --hash=sha256:277cf6aa4b2ad36beed1153cfa7bf521b210d54fbecb3d8eea0c5679cecc9ed8
- # via
- # -r requirements/pytest.pip
- # pycontracts
-readme-renderer==37.2 \
- --hash=sha256:d3f06a69e8c40fca9ab3174eca48f96d9771eddb43517b17d96583418427b106 \
- --hash=sha256:e8ad25293c98f781dbc2c5a36a309929390009f902f99e1798c761aaf04a7923
+pytest-xdist==3.3.1
+ # via -r requirements/pytest.in
+readme-renderer==37.3
# via
# -r requirements/dev.in
# twine
-requests==2.28.1 \
- --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \
- --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349
+requests==2.31.0
# via
# -r requirements/dev.in
# requests-toolbelt
# twine
-requests-toolbelt==0.9.1 \
- --hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f \
- --hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0
+requests-toolbelt==1.0.0
# via twine
-rfc3986==2.0.0 \
- --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \
- --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c
+rfc3986==2.0.0
# via twine
-rich==12.5.1 \
- --hash=sha256:2eb4e6894cde1e017976d2975ac210ef515d7548bc595ba20e195fb9628acdeb \
- --hash=sha256:63a5c5ce3673d3d5fbbf23cd87e11ab84b6b451436f1b7f19ec54b6bc36ed7ca
+rich==13.3.5
# via twine
-six==1.16.0 \
- --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
- --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
- # via
- # -r requirements/pytest.pip
- # bleach
- # libsass
- # pycontracts
- # tox
-sortedcontainers==2.4.0 \
- --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \
- --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0
- # via
- # -r requirements/pytest.pip
- # hypothesis
-tomli==2.0.1 \
- --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
- --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+six==1.16.0
+ # via bleach
+sortedcontainers==2.4.0
+ # via hypothesis
+tomli==2.0.1
# via
- # -r requirements/pytest.pip
# build
# check-manifest
- # pep517
# pylint
+ # pyproject-api
+ # pyproject-hooks
# pytest
# tox
-tomlkit==0.11.4 \
- --hash=sha256:25d4e2e446c453be6360c67ddfb88838cfc42026322770ba13d1fbd403a93a5c \
- --hash=sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83
+tomlkit==0.11.8
# via pylint
-tox==3.26.0 \
- --hash=sha256:44f3c347c68c2c68799d7d44f1808f9d396fc8a1a500cbc624253375c7ae107e \
- --hash=sha256:bf037662d7c740d15c9924ba23bb3e587df20598697bb985ac2b49bdc2d847f6
- # via -r requirements/dev.in
-twine==4.0.1 \
- --hash=sha256:42026c18e394eac3e06693ee52010baa5313e4811d5a11050e7d48436cf41b9e \
- --hash=sha256:96b1cf12f7ae611a4a40b6ae8e9570215daff0611828f5fe1f37a16255ab24a0
+tox==4.5.2
+ # via
+ # -r requirements/tox.in
+ # tox-gh
+tox-gh==1.0.0
+ # via -r requirements/tox.in
+twine==4.0.2
# via -r requirements/dev.in
-typed-ast==1.5.4 \
- --hash=sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2 \
- --hash=sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1 \
- --hash=sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6 \
- --hash=sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62 \
- --hash=sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac \
- --hash=sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d \
- --hash=sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc \
- --hash=sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2 \
- --hash=sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97 \
- --hash=sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35 \
- --hash=sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6 \
- --hash=sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1 \
- --hash=sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4 \
- --hash=sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c \
- --hash=sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e \
- --hash=sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec \
- --hash=sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f \
- --hash=sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72 \
- --hash=sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47 \
- --hash=sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72 \
- --hash=sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe \
- --hash=sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6 \
- --hash=sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3 \
- --hash=sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66
+typed-ast==1.5.4
# via astroid
-typing-extensions==4.3.0 \
- --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \
- --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6
+typing-extensions==4.6.2
# via
- # -r requirements/pip.pip
- # -r requirements/pytest.pip
# astroid
# importlib-metadata
+ # markdown-it-py
+ # platformdirs
# pylint
# rich
-urllib3==1.26.12 \
- --hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \
- --hash=sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997
+ # tox
+urllib3==2.0.2
# via
# requests
# twine
-urwid==2.1.2 \
- --hash=sha256:588bee9c1cb208d0906a9f73c613d2bd32c3ed3702012f51efe318a3f2127eae
+urwid==2.1.2
# via
# pudb
# urwid-readline
-urwid-readline==0.13 \
- --hash=sha256:018020cbc864bb5ed87be17dc26b069eae2755cb29f3a9c569aac3bded1efaf4
+urwid-readline==0.13
# via pudb
-virtualenv==20.16.5 \
- --hash=sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da \
- --hash=sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27
+virtualenv==20.23.0
# via
- # -r requirements/pip.pip
+ # -r requirements/pip.in
# tox
-webencodings==0.5.1 \
- --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
- --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
+webencodings==0.5.1
# via bleach
-wrapt==1.14.1 \
- --hash=sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3 \
- --hash=sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b \
- --hash=sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4 \
- --hash=sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2 \
- --hash=sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656 \
- --hash=sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3 \
- --hash=sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff \
- --hash=sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310 \
- --hash=sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a \
- --hash=sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57 \
- --hash=sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069 \
- --hash=sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383 \
- --hash=sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe \
- --hash=sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87 \
- --hash=sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d \
- --hash=sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b \
- --hash=sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907 \
- --hash=sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f \
- --hash=sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0 \
- --hash=sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28 \
- --hash=sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1 \
- --hash=sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853 \
- --hash=sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc \
- --hash=sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3 \
- --hash=sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3 \
- --hash=sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164 \
- --hash=sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1 \
- --hash=sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c \
- --hash=sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1 \
- --hash=sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7 \
- --hash=sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1 \
- --hash=sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320 \
- --hash=sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed \
- --hash=sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1 \
- --hash=sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248 \
- --hash=sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c \
- --hash=sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456 \
- --hash=sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77 \
- --hash=sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef \
- --hash=sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1 \
- --hash=sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7 \
- --hash=sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86 \
- --hash=sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4 \
- --hash=sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d \
- --hash=sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d \
- --hash=sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8 \
- --hash=sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5 \
- --hash=sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471 \
- --hash=sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00 \
- --hash=sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68 \
- --hash=sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3 \
- --hash=sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d \
- --hash=sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735 \
- --hash=sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d \
- --hash=sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569 \
- --hash=sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7 \
- --hash=sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59 \
- --hash=sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5 \
- --hash=sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb \
- --hash=sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b \
- --hash=sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f \
- --hash=sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462 \
- --hash=sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015 \
- --hash=sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af
+wrapt==1.15.0
# via astroid
-zipp==3.8.1 \
- --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \
- --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009
+zipp==3.15.0
# via
- # -r requirements/pip.pip
- # -r requirements/pytest.pip
# importlib-metadata
- # pep517
+ # importlib-resources
# The following packages are considered to be unsafe in a requirements file:
-pip==22.0.4 \
- --hash=sha256:b3a9de2c6ef801e9247d1527a4b16f92f2cc141cd1489f3fffaf6a9e96729764 \
- --hash=sha256:c6aca0f2f081363f689f041d90dab2a07a9a07fb840284db2218117a52da800b
- # via
- # -c requirements/pins.pip
- # -r requirements/pip.pip
-setuptools==65.4.0 \
- --hash=sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9 \
- --hash=sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1
- # via check-manifest
+pip==23.1.2
+ # via -r requirements/pip.in
+setuptools==67.8.0
+ # via
+ # -r requirements/pip.in
+ # check-manifest
diff -Nru python-coverage-6.5.0+dfsg1/requirements/kit.pip python-coverage-7.2.7+dfsg1/requirements/kit.pip
--- python-coverage-6.5.0+dfsg1/requirements/kit.pip 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/requirements/kit.pip 2023-05-29 19:46:30.000000000 +0000
@@ -1,98 +1,54 @@
#
-# This file is autogenerated by pip-compile with python 3.7
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.7
+# by the following command:
#
# make upgrade
#
-auditwheel==5.1.2 \
- --hash=sha256:3ee5830014931ea84af5cd065c637b6614efa03d9b88bd8fbfc924e7ed01d6ba \
- --hash=sha256:4d06aea3ab59a2b8aa733798ac221556a3f5c021fddc42e5de5bcef20201c031
- # via -r requirements/kit.in
-bashlex==0.16 \
- --hash=sha256:dc6f017e49ce2d0fe30ad9f5206da9cd13ded073d365688c9fda525354e8c373 \
- --hash=sha256:ff89fc743ccdef978792784d74d698a9236a862939bb4af471c0c3faf92c21bb
+auditwheel==5.4.0
+ # via -r requirements/kit.in
+bashlex==0.18
# via cibuildwheel
-bracex==2.3.post1 \
- --hash=sha256:351b7f20d56fb9ea91f9b9e9e7664db466eb234188c175fd943f8f755c807e73 \
- --hash=sha256:e7b23fc8b2cd06d3dec0692baabecb249dda94e06a617901ff03a6c56fd71693
+bracex==2.3.post1
# via cibuildwheel
-build==0.8.0 \
- --hash=sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437 \
- --hash=sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0
- # via -r requirements/kit.in
-certifi==2022.5.18.1 \
- --hash=sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7 \
- --hash=sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a
- # via
- # -c requirements/pins.pip
- # cibuildwheel
-cibuildwheel==2.10.2 \
- --hash=sha256:63ff185b4bc1ec62a87309411cc4bc297e6cfab051a3953ab1c7f0d6394b8da3 \
- --hash=sha256:d333005672c58f86b54c944983b495a91138f30cdbcaee47699ad9a29abc345d
- # via -r requirements/kit.in
-colorama==0.4.5 \
- --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \
- --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4
- # via -r requirements/kit.in
-filelock==3.8.0 \
- --hash=sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc \
- --hash=sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4
+build==0.10.0
+ # via -r requirements/kit.in
+certifi==2023.5.7
+ # via cibuildwheel
+cibuildwheel==2.13.0
+ # via -r requirements/kit.in
+colorama==0.4.6
+ # via -r requirements/kit.in
+filelock==3.12.0
# via cibuildwheel
-importlib-metadata==4.12.0 \
- --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \
- --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23
+importlib-metadata==6.6.0
# via
# auditwheel
# build
- # pep517
-packaging==21.3 \
- --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
- --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
+packaging==23.1
# via
# build
# cibuildwheel
-pep517==0.13.0 \
- --hash=sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b \
- --hash=sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59
- # via build
-platformdirs==2.5.2 \
- --hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
- --hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19
+platformdirs==3.5.1
# via cibuildwheel
-pyelftools==0.29 \
- --hash=sha256:519f38cf412f073b2d7393aa4682b0190fa901f7c3fa0bff2b82d537690c7fc1 \
- --hash=sha256:ec761596aafa16e282a31de188737e5485552469ac63b60cfcccf22263fd24ff
+pyelftools==0.29
# via auditwheel
-pyparsing==3.0.9 \
- --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
- --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
- # via packaging
-tomli==2.0.1 \
- --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
- --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+pyproject-hooks==1.0.0
+ # via build
+tomli==2.0.1
# via
# build
# cibuildwheel
- # pep517
-typing-extensions==4.3.0 \
- --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \
- --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6
+ # pyproject-hooks
+typing-extensions==4.6.2
# via
# cibuildwheel
# importlib-metadata
-wheel==0.37.1 \
- --hash=sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a \
- --hash=sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4
- # via -r requirements/kit.in
-zipp==3.8.1 \
- --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \
- --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009
- # via
- # importlib-metadata
- # pep517
+ # platformdirs
+wheel==0.40.0
+ # via -r requirements/kit.in
+zipp==3.15.0
+ # via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
-setuptools==65.4.0 \
- --hash=sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9 \
- --hash=sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1
+setuptools==67.8.0
# via -r requirements/kit.in
diff -Nru python-coverage-6.5.0+dfsg1/requirements/light-threads.pip python-coverage-7.2.7+dfsg1/requirements/light-threads.pip
--- python-coverage-6.5.0+dfsg1/requirements/light-threads.pip 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/requirements/light-threads.pip 2023-05-29 19:46:30.000000000 +0000
@@ -1,245 +1,34 @@
#
-# This file is autogenerated by pip-compile with python 3.7
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.7
+# by the following command:
#
# make upgrade
#
-cffi==1.15.1 \
- --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \
- --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \
- --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \
- --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \
- --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \
- --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \
- --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \
- --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \
- --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \
- --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \
- --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \
- --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \
- --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \
- --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \
- --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \
- --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \
- --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \
- --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \
- --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \
- --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \
- --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \
- --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \
- --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \
- --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \
- --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \
- --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \
- --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \
- --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \
- --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \
- --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \
- --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \
- --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \
- --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \
- --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \
- --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \
- --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \
- --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \
- --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \
- --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \
- --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \
- --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \
- --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \
- --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \
- --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \
- --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \
- --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \
- --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \
- --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \
- --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \
- --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \
- --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \
- --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \
- --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \
- --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \
- --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \
- --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \
- --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \
- --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \
- --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \
- --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \
- --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \
- --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \
- --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \
- --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0
+cffi==1.15.1
# via -r requirements/light-threads.in
-dnspython==2.2.1 \
- --hash=sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e \
- --hash=sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f
+dnspython==2.3.0
# via eventlet
-eventlet==0.33.1 \
- --hash=sha256:a085922698e5029f820cf311a648ac324d73cec0e4792877609d978a4b5bbf31 \
- --hash=sha256:afbe17f06a58491e9aebd7a4a03e70b0b63fd4cf76d8307bae07f280479b1515
+eventlet==0.33.3
# via -r requirements/light-threads.in
-gevent==21.12.0 \
- --hash=sha256:0082d8a5d23c35812ce0e716a91ede597f6dd2c5ff508a02a998f73598c59397 \
- --hash=sha256:01928770972181ad8866ee37ea3504f1824587b188fcab782ef1619ce7538766 \
- --hash=sha256:05c5e8a50cd6868dd36536c92fb4468d18090e801bd63611593c0717bab63692 \
- --hash=sha256:08b4c17064e28f4eb85604486abc89f442c7407d2aed249cf54544ce5c9baee6 \
- --hash=sha256:177f93a3a90f46a5009e0841fef561601e5c637ba4332ab8572edd96af650101 \
- --hash=sha256:22ce1f38fdfe2149ffe8ec2131ca45281791c1e464db34b3b4321ae9d8d2efbb \
- --hash=sha256:24d3550fbaeef5fddd794819c2853bca45a86c3d64a056a2c268d981518220d1 \
- --hash=sha256:2afa3f3ad528155433f6ac8bd64fa5cc303855b97004416ec719a6b1ca179481 \
- --hash=sha256:2bcec9f80196c751fdcf389ca9f7141e7b0db960d8465ed79be5e685bfcad682 \
- --hash=sha256:2cfff82f05f14b7f5d9ed53ccb7a609ae8604df522bb05c971bca78ec9d8b2b9 \
- --hash=sha256:3baeeccc4791ba3f8db27179dff11855a8f9210ddd754f6c9b48e0d2561c2aea \
- --hash=sha256:3c012c73e6c61f13c75e3a4869dbe6a2ffa025f103421a6de9c85e627e7477b1 \
- --hash=sha256:3dad62f55fad839d498c801e139481348991cee6e1c7706041b5fe096cb6a279 \
- --hash=sha256:542ae891e2aa217d2cf6d8446538fcd2f3263a40eec123b970b899bac391c47a \
- --hash=sha256:6a02a88723ed3f0fd92cbf1df3c4cd2fbd87d82b0a4bac3e36a8875923115214 \
- --hash=sha256:74fc1ef16b86616cfddcc74f7292642b0f72dde4dd95aebf4c45bb236744be54 \
- --hash=sha256:7909780f0cf18a1fc32aafd8c8e130cdd93c6e285b11263f7f2d1a0f3678bc50 \
- --hash=sha256:7ccffcf708094564e442ac6fde46f0ae9e40015cb69d995f4b39cc29a7643881 \
- --hash=sha256:8c21cb5c9f4e14d75b3fe0b143ec875d7dbd1495fad6d49704b00e57e781ee0f \
- --hash=sha256:973749bacb7bc4f4181a8fb2a7e0e2ff44038de56d08e856dd54a5ac1d7331b4 \
- --hash=sha256:9d86438ede1cbe0fde6ef4cc3f72bf2f1ecc9630d8b633ff344a3aeeca272cdd \
- --hash=sha256:9f9652d1e4062d4b5b5a0a49ff679fa890430b5f76969d35dccb2df114c55e0f \
- --hash=sha256:a5ad4ed8afa0a71e1927623589f06a9b5e8b5e77810be3125cb4d93050d3fd1f \
- --hash=sha256:b7709c64afa8bb3000c28bb91ec42c79594a7cb0f322e20427d57f9762366a5b \
- --hash=sha256:bb5cb8db753469c7a9a0b8a972d2660fe851aa06eee699a1ca42988afb0aaa02 \
- --hash=sha256:c43f081cbca41d27fd8fef9c6a32cf83cb979345b20abc07bf68df165cdadb24 \
- --hash=sha256:cc2fef0f98ee180704cf95ec84f2bc2d86c6c3711bb6b6740d74e0afe708b62c \
- --hash=sha256:da8d2d51a49b2a5beb02ad619ca9ddbef806ef4870ba04e5ac7b8b41a5b61db3 \
- --hash=sha256:e1899b921219fc8959ff9afb94dae36be82e0769ed13d330a393594d478a0b3a \
- --hash=sha256:eae3c46f9484eaacd67ffcdf4eaf6ca830f587edd543613b0f5c4eb3c11d052d \
- --hash=sha256:ec21f9eaaa6a7b1e62da786132d6788675b314f25f98d9541f1bf00584ed4749 \
- --hash=sha256:f289fae643a3f1c3b909d6b033e6921b05234a4907e9c9c8c3f1fe403e6ac452 \
- --hash=sha256:f48b64578c367b91fa793bf8eaaaf4995cb93c8bc45860e473bf868070ad094e
+gevent==22.10.2
# via -r requirements/light-threads.in
-greenlet==1.1.3 \
- --hash=sha256:0118817c9341ef2b0f75f5af79ac377e4da6ff637e5ee4ac91802c0e379dadb4 \
- --hash=sha256:048d2bed76c2aa6de7af500ae0ea51dd2267aec0e0f2a436981159053d0bc7cc \
- --hash=sha256:07c58e169bbe1e87b8bbf15a5c1b779a7616df9fd3e61cadc9d691740015b4f8 \
- --hash=sha256:095a980288fe05adf3d002fbb180c99bdcf0f930e220aa66fcd56e7914a38202 \
- --hash=sha256:0b181e9aa6cb2f5ec0cacc8cee6e5a3093416c841ba32c185c30c160487f0380 \
- --hash=sha256:1626185d938d7381631e48e6f7713e8d4b964be246073e1a1d15c2f061ac9f08 \
- --hash=sha256:184416e481295832350a4bf731ba619a92f5689bf5d0fa4341e98b98b1265bd7 \
- --hash=sha256:1dd51d2650e70c6c4af37f454737bf4a11e568945b27f74b471e8e2a9fd21268 \
- --hash=sha256:1ec2779774d8e42ed0440cf8bc55540175187e8e934f2be25199bf4ed948cd9e \
- --hash=sha256:2cf45e339cabea16c07586306a31cfcc5a3b5e1626d365714d283732afed6809 \
- --hash=sha256:2fb0aa7f6996879551fd67461d5d3ab0c3c0245da98be90c89fcb7a18d437403 \
- --hash=sha256:44b4817c34c9272c65550b788913620f1fdc80362b209bc9d7dd2f40d8793080 \
- --hash=sha256:466ce0928e33421ee84ae04c4ac6f253a3a3e6b8d600a79bd43fd4403e0a7a76 \
- --hash=sha256:4f166b4aca8d7d489e82d74627a7069ab34211ef5ebb57c300ec4b9337b60fc0 \
- --hash=sha256:510c3b15587afce9800198b4b142202b323bf4b4b5f9d6c79cb9a35e5e3c30d2 \
- --hash=sha256:5b756e6730ea59b2745072e28ad27f4c837084688e6a6b3633c8b1e509e6ae0e \
- --hash=sha256:5fbe1ab72b998ca77ceabbae63a9b2e2dc2d963f4299b9b278252ddba142d3f1 \
- --hash=sha256:6200a11f003ec26815f7e3d2ded01b43a3810be3528dd760d2f1fa777490c3cd \
- --hash=sha256:65ad1a7a463a2a6f863661329a944a5802c7129f7ad33583dcc11069c17e622c \
- --hash=sha256:694ffa7144fa5cc526c8f4512665003a39fa09ef00d19bbca5c8d3406db72fbe \
- --hash=sha256:6f5d4b2280ceea76c55c893827961ed0a6eadd5a584a7c4e6e6dd7bc10dfdd96 \
- --hash=sha256:7532a46505470be30cbf1dbadb20379fb481244f1ca54207d7df3bf0bbab6a20 \
- --hash=sha256:76a53bfa10b367ee734b95988bd82a9a5f0038a25030f9f23bbbc005010ca600 \
- --hash=sha256:77e41db75f9958f2083e03e9dd39da12247b3430c92267df3af77c83d8ff9eed \
- --hash=sha256:7a43bbfa9b6cfdfaeefbd91038dde65ea2c421dc387ed171613df340650874f2 \
- --hash=sha256:7b41d19c0cfe5c259fe6c539fd75051cd39a5d33d05482f885faf43f7f5e7d26 \
- --hash=sha256:7c5227963409551ae4a6938beb70d56bf1918c554a287d3da6853526212fbe0a \
- --hash=sha256:870a48007872d12e95a996fca3c03a64290d3ea2e61076aa35d3b253cf34cd32 \
- --hash=sha256:88b04e12c9b041a1e0bcb886fec709c488192638a9a7a3677513ac6ba81d8e79 \
- --hash=sha256:8c287ae7ac921dfde88b1c125bd9590b7ec3c900c2d3db5197f1286e144e712b \
- --hash=sha256:903fa5716b8fbb21019268b44f73f3748c41d1a30d71b4a49c84b642c2fed5fa \
- --hash=sha256:9537e4baf0db67f382eb29255a03154fcd4984638303ff9baaa738b10371fa57 \
- --hash=sha256:9951dcbd37850da32b2cb6e391f621c1ee456191c6ae5528af4a34afe357c30e \
- --hash=sha256:9b2f7d0408ddeb8ea1fd43d3db79a8cefaccadd2a812f021333b338ed6b10aba \
- --hash=sha256:9c88e134d51d5e82315a7c32b914a58751b7353eb5268dbd02eabf020b4c4700 \
- --hash=sha256:9fae214f6c43cd47f7bef98c56919b9222481e833be2915f6857a1e9e8a15318 \
- --hash=sha256:a3a669f11289a8995d24fbfc0e63f8289dd03c9aaa0cc8f1eab31d18ca61a382 \
- --hash=sha256:aa741c1a8a8cc25eb3a3a01a62bdb5095a773d8c6a86470bde7f607a447e7905 \
- --hash=sha256:b0877a9a2129a2c56a2eae2da016743db7d9d6a05d5e1c198f1b7808c602a30e \
- --hash=sha256:bcb6c6dd1d6be6d38d6db283747d07fda089ff8c559a835236560a4410340455 \
- --hash=sha256:caff52cb5cd7626872d9696aee5b794abe172804beb7db52eed1fd5824b63910 \
- --hash=sha256:cbc1eb55342cbac8f7ec159088d54e2cfdd5ddf61c87b8bbe682d113789331b2 \
- --hash=sha256:cd16a89efe3a003029c87ff19e9fba635864e064da646bc749fc1908a4af18f3 \
- --hash=sha256:ce5b64dfe8d0cca407d88b0ee619d80d4215a2612c1af8c98a92180e7109f4b5 \
- --hash=sha256:d58a5a71c4c37354f9e0c24c9c8321f0185f6945ef027460b809f4bb474bfe41 \
- --hash=sha256:db41f3845eb579b544c962864cce2c2a0257fe30f0f1e18e51b1e8cbb4e0ac6d \
- --hash=sha256:db5b25265010a1b3dca6a174a443a0ed4c4ab12d5e2883a11c97d6e6d59b12f9 \
- --hash=sha256:dd0404d154084a371e6d2bafc787201612a1359c2dee688ae334f9118aa0bf47 \
- --hash=sha256:de431765bd5fe62119e0bc6bc6e7b17ac53017ae1782acf88fcf6b7eae475a49 \
- --hash=sha256:df02fdec0c533301497acb0bc0f27f479a3a63dcdc3a099ae33a902857f07477 \
- --hash=sha256:e8533f5111704d75de3139bf0b8136d3a6c1642c55c067866fa0a51c2155ee33 \
- --hash=sha256:f2f908239b7098799b8845e5936c2ccb91d8c2323be02e82f8dcb4a80dcf4a25 \
- --hash=sha256:f8bfd36f368efe0ab2a6aa3db7f14598aac454b06849fb633b762ddbede1db90 \
- --hash=sha256:ffe73f9e7aea404722058405ff24041e59d31ca23d1da0895af48050a07b6932
+greenlet==2.0.2
# via
# -r requirements/light-threads.in
# eventlet
# gevent
-pycparser==2.21 \
- --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
- --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
+pycparser==2.21
# via cffi
-six==1.16.0 \
- --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
- --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+six==1.16.0
# via eventlet
-zope-event==4.5.0 \
- --hash=sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42 \
- --hash=sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330
+zope-event==4.6
# via gevent
-zope-interface==5.4.0 \
- --hash=sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192 \
- --hash=sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702 \
- --hash=sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09 \
- --hash=sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4 \
- --hash=sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a \
- --hash=sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3 \
- --hash=sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf \
- --hash=sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c \
- --hash=sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d \
- --hash=sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78 \
- --hash=sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83 \
- --hash=sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531 \
- --hash=sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46 \
- --hash=sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021 \
- --hash=sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94 \
- --hash=sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc \
- --hash=sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63 \
- --hash=sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54 \
- --hash=sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117 \
- --hash=sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25 \
- --hash=sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05 \
- --hash=sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e \
- --hash=sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1 \
- --hash=sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004 \
- --hash=sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2 \
- --hash=sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e \
- --hash=sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f \
- --hash=sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f \
- --hash=sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120 \
- --hash=sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f \
- --hash=sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1 \
- --hash=sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9 \
- --hash=sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e \
- --hash=sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7 \
- --hash=sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8 \
- --hash=sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b \
- --hash=sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155 \
- --hash=sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7 \
- --hash=sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c \
- --hash=sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325 \
- --hash=sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d \
- --hash=sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb \
- --hash=sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e \
- --hash=sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959 \
- --hash=sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7 \
- --hash=sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920 \
- --hash=sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e \
- --hash=sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48 \
- --hash=sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8 \
- --hash=sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4 \
- --hash=sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263
+zope-interface==6.0
# via gevent
# The following packages are considered to be unsafe in a requirements file:
-setuptools==65.4.0 \
- --hash=sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9 \
- --hash=sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1
- # via gevent
+setuptools==67.8.0
+ # via
+ # gevent
+ # zope-event
+ # zope-interface
diff -Nru python-coverage-6.5.0+dfsg1/requirements/lint.pip python-coverage-7.2.7+dfsg1/requirements/lint.pip
--- python-coverage-6.5.0+dfsg1/requirements/lint.pip 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/requirements/lint.pip 2023-05-29 19:46:30.000000000 +0000
@@ -1,204 +1,84 @@
#
-# This file is autogenerated by pip-compile with python 3.7
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.7
+# by the following command:
#
# make upgrade
#
-alabaster==0.7.12 \
- --hash=sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359 \
- --hash=sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02
+alabaster==0.7.13
# via sphinx
-astroid==2.12.10 \
- --hash=sha256:81f870105d892e73bf535da77a8261aa5bde838fa4ed12bb2f435291a098c581 \
- --hash=sha256:997e0c735df60d4a4caff27080a3afc51f9bdd693d3572a4a0b7090b645c36c5
+astroid==2.15.5
# via pylint
-atomicwrites==1.4.1 \
- --hash=sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11
- # via -r requirements/pytest.pip
-attrs==22.1.0 \
- --hash=sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6 \
- --hash=sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c
+attrs==23.1.0
# via
- # -r requirements/pytest.pip
# hypothesis
- # pytest
-babel==2.10.3 \
- --hash=sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51 \
- --hash=sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb
+ # scriv
+babel==2.12.1
# via sphinx
-backports-functools-lru-cache==1.6.4 \
- --hash=sha256:d5ed2169378b67d3c545e5600d363a923b09c456dab1593914935a68ad478271 \
- --hash=sha256:dbead04b9daa817909ec64e8d2855fb78feafe0b901d4568758e3a60559d8978
- # via
- # -r requirements/pytest.pip
- # pycontracts
-bleach==5.0.1 \
- --hash=sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a \
- --hash=sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c
+bleach==6.0.0
# via readme-renderer
-build==0.8.0 \
- --hash=sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437 \
- --hash=sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0
+build==0.10.0
# via check-manifest
-certifi==2022.5.18.1 \
- --hash=sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7 \
- --hash=sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a
- # via
- # -c doc/../requirements/pins.pip
- # -c requirements/pins.pip
- # requests
-charset-normalizer==2.1.1 \
- --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \
- --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f
+cachetools==5.3.1
+ # via tox
+certifi==2023.5.7
# via requests
-check-manifest==0.48 \
- --hash=sha256:3b575f1dade7beb3078ef4bf33a94519834457c7281dbc726b15c5466b55c657 \
- --hash=sha256:b1923685f98c1c2468601a1a7bed655db549a25d43c583caded3860ad8308f8c
+chardet==5.1.0
+ # via tox
+charset-normalizer==3.1.0
+ # via requests
+check-manifest==0.49
# via -r requirements/dev.in
-cogapp==3.3.0 \
- --hash=sha256:1be95183f70282422d594fa42426be6923070a4bd8335621f6347f3aeee81db0 \
- --hash=sha256:8b5b5f6063d8ee231961c05da010cb27c30876b2279e23ad0eae5f8f09460d50
+click==8.1.3
+ # via
+ # click-log
+ # scriv
+click-log==0.4.0
+ # via scriv
+cogapp==3.3.0
# via
# -r doc/requirements.in
# -r requirements/dev.in
-colorama==0.4.5 \
- --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \
- --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4
+colorama==0.4.6
# via
- # -r requirements/pytest.pip
+ # -r requirements/pytest.in
+ # -r requirements/tox.in
# sphinx-autobuild
-commonmark==0.9.1 \
- --hash=sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60 \
- --hash=sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9
- # via rich
-decorator==5.1.1 \
- --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \
- --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186
- # via
- # -r requirements/pytest.pip
- # pycontracts
-dill==0.3.5.1 \
- --hash=sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302 \
- --hash=sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86
+ # tox
+dill==0.3.6
# via pylint
-distlib==0.3.6 \
- --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \
- --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e
- # via
- # -r requirements/pip.pip
- # virtualenv
-docutils==0.17.1 \
- --hash=sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125 \
- --hash=sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61
+distlib==0.3.6
+ # via virtualenv
+docutils==0.18.1
# via
# readme-renderer
# sphinx
# sphinx-rtd-theme
-exceptiongroup==1.0.0rc9 \
- --hash=sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337 \
- --hash=sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96
+exceptiongroup==1.1.1
# via
- # -r requirements/pytest.pip
# hypothesis
-execnet==1.9.0 \
- --hash=sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5 \
- --hash=sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142
- # via
- # -r requirements/pytest.pip
- # pytest-xdist
-filelock==3.8.0 \
- --hash=sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc \
- --hash=sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4
+ # pytest
+execnet==1.9.0
+ # via pytest-xdist
+filelock==3.12.0
# via
- # -r requirements/pip.pip
# tox
# virtualenv
-flaky==3.7.0 \
- --hash=sha256:3ad100780721a1911f57a165809b7ea265a7863305acb66708220820caf8aa0d \
- --hash=sha256:d6eda73cab5ae7364504b7c44670f70abed9e75f77dd116352f662817592ec9c
- # via -r requirements/pytest.pip
-future==0.18.2 \
- --hash=sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d
- # via
- # -r requirements/pytest.pip
- # pycontracts
-greenlet==1.1.3 \
- --hash=sha256:0118817c9341ef2b0f75f5af79ac377e4da6ff637e5ee4ac91802c0e379dadb4 \
- --hash=sha256:048d2bed76c2aa6de7af500ae0ea51dd2267aec0e0f2a436981159053d0bc7cc \
- --hash=sha256:07c58e169bbe1e87b8bbf15a5c1b779a7616df9fd3e61cadc9d691740015b4f8 \
- --hash=sha256:095a980288fe05adf3d002fbb180c99bdcf0f930e220aa66fcd56e7914a38202 \
- --hash=sha256:0b181e9aa6cb2f5ec0cacc8cee6e5a3093416c841ba32c185c30c160487f0380 \
- --hash=sha256:1626185d938d7381631e48e6f7713e8d4b964be246073e1a1d15c2f061ac9f08 \
- --hash=sha256:184416e481295832350a4bf731ba619a92f5689bf5d0fa4341e98b98b1265bd7 \
- --hash=sha256:1dd51d2650e70c6c4af37f454737bf4a11e568945b27f74b471e8e2a9fd21268 \
- --hash=sha256:1ec2779774d8e42ed0440cf8bc55540175187e8e934f2be25199bf4ed948cd9e \
- --hash=sha256:2cf45e339cabea16c07586306a31cfcc5a3b5e1626d365714d283732afed6809 \
- --hash=sha256:2fb0aa7f6996879551fd67461d5d3ab0c3c0245da98be90c89fcb7a18d437403 \
- --hash=sha256:44b4817c34c9272c65550b788913620f1fdc80362b209bc9d7dd2f40d8793080 \
- --hash=sha256:466ce0928e33421ee84ae04c4ac6f253a3a3e6b8d600a79bd43fd4403e0a7a76 \
- --hash=sha256:4f166b4aca8d7d489e82d74627a7069ab34211ef5ebb57c300ec4b9337b60fc0 \
- --hash=sha256:510c3b15587afce9800198b4b142202b323bf4b4b5f9d6c79cb9a35e5e3c30d2 \
- --hash=sha256:5b756e6730ea59b2745072e28ad27f4c837084688e6a6b3633c8b1e509e6ae0e \
- --hash=sha256:5fbe1ab72b998ca77ceabbae63a9b2e2dc2d963f4299b9b278252ddba142d3f1 \
- --hash=sha256:6200a11f003ec26815f7e3d2ded01b43a3810be3528dd760d2f1fa777490c3cd \
- --hash=sha256:65ad1a7a463a2a6f863661329a944a5802c7129f7ad33583dcc11069c17e622c \
- --hash=sha256:694ffa7144fa5cc526c8f4512665003a39fa09ef00d19bbca5c8d3406db72fbe \
- --hash=sha256:6f5d4b2280ceea76c55c893827961ed0a6eadd5a584a7c4e6e6dd7bc10dfdd96 \
- --hash=sha256:7532a46505470be30cbf1dbadb20379fb481244f1ca54207d7df3bf0bbab6a20 \
- --hash=sha256:76a53bfa10b367ee734b95988bd82a9a5f0038a25030f9f23bbbc005010ca600 \
- --hash=sha256:77e41db75f9958f2083e03e9dd39da12247b3430c92267df3af77c83d8ff9eed \
- --hash=sha256:7a43bbfa9b6cfdfaeefbd91038dde65ea2c421dc387ed171613df340650874f2 \
- --hash=sha256:7b41d19c0cfe5c259fe6c539fd75051cd39a5d33d05482f885faf43f7f5e7d26 \
- --hash=sha256:7c5227963409551ae4a6938beb70d56bf1918c554a287d3da6853526212fbe0a \
- --hash=sha256:870a48007872d12e95a996fca3c03a64290d3ea2e61076aa35d3b253cf34cd32 \
- --hash=sha256:88b04e12c9b041a1e0bcb886fec709c488192638a9a7a3677513ac6ba81d8e79 \
- --hash=sha256:8c287ae7ac921dfde88b1c125bd9590b7ec3c900c2d3db5197f1286e144e712b \
- --hash=sha256:903fa5716b8fbb21019268b44f73f3748c41d1a30d71b4a49c84b642c2fed5fa \
- --hash=sha256:9537e4baf0db67f382eb29255a03154fcd4984638303ff9baaa738b10371fa57 \
- --hash=sha256:9951dcbd37850da32b2cb6e391f621c1ee456191c6ae5528af4a34afe357c30e \
- --hash=sha256:9b2f7d0408ddeb8ea1fd43d3db79a8cefaccadd2a812f021333b338ed6b10aba \
- --hash=sha256:9c88e134d51d5e82315a7c32b914a58751b7353eb5268dbd02eabf020b4c4700 \
- --hash=sha256:9fae214f6c43cd47f7bef98c56919b9222481e833be2915f6857a1e9e8a15318 \
- --hash=sha256:a3a669f11289a8995d24fbfc0e63f8289dd03c9aaa0cc8f1eab31d18ca61a382 \
- --hash=sha256:aa741c1a8a8cc25eb3a3a01a62bdb5095a773d8c6a86470bde7f607a447e7905 \
- --hash=sha256:b0877a9a2129a2c56a2eae2da016743db7d9d6a05d5e1c198f1b7808c602a30e \
- --hash=sha256:bcb6c6dd1d6be6d38d6db283747d07fda089ff8c559a835236560a4410340455 \
- --hash=sha256:caff52cb5cd7626872d9696aee5b794abe172804beb7db52eed1fd5824b63910 \
- --hash=sha256:cbc1eb55342cbac8f7ec159088d54e2cfdd5ddf61c87b8bbe682d113789331b2 \
- --hash=sha256:cd16a89efe3a003029c87ff19e9fba635864e064da646bc749fc1908a4af18f3 \
- --hash=sha256:ce5b64dfe8d0cca407d88b0ee619d80d4215a2612c1af8c98a92180e7109f4b5 \
- --hash=sha256:d58a5a71c4c37354f9e0c24c9c8321f0185f6945ef027460b809f4bb474bfe41 \
- --hash=sha256:db41f3845eb579b544c962864cce2c2a0257fe30f0f1e18e51b1e8cbb4e0ac6d \
- --hash=sha256:db5b25265010a1b3dca6a174a443a0ed4c4ab12d5e2883a11c97d6e6d59b12f9 \
- --hash=sha256:dd0404d154084a371e6d2bafc787201612a1359c2dee688ae334f9118aa0bf47 \
- --hash=sha256:de431765bd5fe62119e0bc6bc6e7b17ac53017ae1782acf88fcf6b7eae475a49 \
- --hash=sha256:df02fdec0c533301497acb0bc0f27f479a3a63dcdc3a099ae33a902857f07477 \
- --hash=sha256:e8533f5111704d75de3139bf0b8136d3a6c1642c55c067866fa0a51c2155ee33 \
- --hash=sha256:f2f908239b7098799b8845e5936c2ccb91d8c2323be02e82f8dcb4a80dcf4a25 \
- --hash=sha256:f8bfd36f368efe0ab2a6aa3db7f14598aac454b06849fb633b762ddbede1db90 \
- --hash=sha256:ffe73f9e7aea404722058405ff24041e59d31ca23d1da0895af48050a07b6932
+flaky==3.7.0
+ # via -r requirements/pytest.in
+greenlet==2.0.2
# via -r requirements/dev.in
-hypothesis==6.54.6 \
- --hash=sha256:2d5e2d5ccd0efce4e0968a6164f4e4853f808e33f4d91490c975c98beec0c7c3 \
- --hash=sha256:e44833325f9a55f795596ceefd7ede7d626cfe45836025d2647cccaff7070e10
- # via -r requirements/pytest.pip
-idna==3.4 \
- --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
- --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+hypothesis==6.75.6
+ # via -r requirements/pytest.in
+idna==3.4
# via requests
-imagesize==1.4.1 \
- --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \
- --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a
+imagesize==1.4.1
# via sphinx
-importlib-metadata==4.12.0 \
- --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \
- --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23
+importlib-metadata==6.6.0
# via
- # -r requirements/pip.pip
- # -r requirements/pytest.pip
+ # attrs
# build
+ # click
# keyring
- # pep517
# pluggy
# pytest
# sphinx
@@ -206,514 +86,201 @@
# tox
# twine
# virtualenv
-iniconfig==1.1.1 \
- --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \
- --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32
- # via
- # -r requirements/pytest.pip
- # pytest
-isort==5.10.1 \
- --hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
- --hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
+importlib-resources==5.12.0
+ # via keyring
+iniconfig==2.0.0
+ # via pytest
+isort==5.11.5
# via pylint
-jaraco-classes==3.2.3 \
- --hash=sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158 \
- --hash=sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a
+jaraco-classes==3.2.3
# via keyring
-jedi==0.18.1 \
- --hash=sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d \
- --hash=sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab
+jedi==0.18.2
# via pudb
-jinja2==3.1.2 \
- --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
- --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
- # via sphinx
-keyring==23.9.3 \
- --hash=sha256:69732a15cb1433bdfbc3b980a8a36a04878a6cfd7cb99f497b573f31618001c0 \
- --hash=sha256:69b01dd83c42f590250fe7a1f503fc229b14de83857314b1933a3ddbf595c4a5
+jinja2==3.1.2
+ # via
+ # scriv
+ # sphinx
+keyring==23.13.1
# via twine
-lazy-object-proxy==1.7.1 \
- --hash=sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7 \
- --hash=sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a \
- --hash=sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c \
- --hash=sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc \
- --hash=sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f \
- --hash=sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09 \
- --hash=sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442 \
- --hash=sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e \
- --hash=sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029 \
- --hash=sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61 \
- --hash=sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb \
- --hash=sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0 \
- --hash=sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35 \
- --hash=sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42 \
- --hash=sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1 \
- --hash=sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad \
- --hash=sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443 \
- --hash=sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd \
- --hash=sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9 \
- --hash=sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148 \
- --hash=sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38 \
- --hash=sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55 \
- --hash=sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36 \
- --hash=sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a \
- --hash=sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b \
- --hash=sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44 \
- --hash=sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6 \
- --hash=sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69 \
- --hash=sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4 \
- --hash=sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84 \
- --hash=sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de \
- --hash=sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28 \
- --hash=sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c \
- --hash=sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1 \
- --hash=sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8 \
- --hash=sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b \
- --hash=sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb
+lazy-object-proxy==1.9.0
# via astroid
-libsass==0.21.0 \
- --hash=sha256:06c8776417fe930714bdc930a3d7e795ae3d72be6ac883ff72a1b8f7c49e5ffb \
- --hash=sha256:12f39712de38689a8b785b7db41d3ba2ea1d46f9379d81ea4595802d91fa6529 \
- --hash=sha256:1e25dd9047a9392d3c59a0b869e0404f2b325a03871ee45285ee33b3664f5613 \
- --hash=sha256:659ae41af8708681fa3ec73f47b9735a6725e71c3b66ff570bfce78952f2314e \
- --hash=sha256:6b984510ed94993708c0d697b4fef2d118929bbfffc3b90037be0f5ccadf55e7 \
- --hash=sha256:a005f298f64624f313a3ac618ab03f844c71d84ae4f4a4aec4b68d2a4ffe75eb \
- --hash=sha256:abc29357ee540849faf1383e1746d40d69ed5cb6d4c346df276b258f5aa8977a \
- --hash=sha256:c9ec490609752c1d81ff6290da33485aa7cb6d7365ac665b74464c1b7d97f7da \
- --hash=sha256:d5ba529d9ce668be9380563279f3ffe988f27bc5b299c5a28453df2e0b0fbaf2 \
- --hash=sha256:e2b1a7d093f2e76dc694c17c0c285e846d0b0deb0e8b21dc852ba1a3a4e2f1d6
+libsass==0.22.0
# via -r requirements/dev.in
-livereload==2.6.3 \
- --hash=sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869
+livereload==2.6.3
# via sphinx-autobuild
-markupsafe==2.1.1 \
- --hash=sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003 \
- --hash=sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88 \
- --hash=sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5 \
- --hash=sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7 \
- --hash=sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a \
- --hash=sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603 \
- --hash=sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1 \
- --hash=sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135 \
- --hash=sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247 \
- --hash=sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6 \
- --hash=sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601 \
- --hash=sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77 \
- --hash=sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02 \
- --hash=sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e \
- --hash=sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63 \
- --hash=sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f \
- --hash=sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980 \
- --hash=sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b \
- --hash=sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812 \
- --hash=sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff \
- --hash=sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96 \
- --hash=sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1 \
- --hash=sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925 \
- --hash=sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a \
- --hash=sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6 \
- --hash=sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e \
- --hash=sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f \
- --hash=sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4 \
- --hash=sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f \
- --hash=sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3 \
- --hash=sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c \
- --hash=sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a \
- --hash=sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417 \
- --hash=sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a \
- --hash=sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a \
- --hash=sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37 \
- --hash=sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452 \
- --hash=sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933 \
- --hash=sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a \
- --hash=sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7
+markdown-it-py==2.2.0
+ # via rich
+markupsafe==2.1.2
# via jinja2
-mccabe==0.7.0 \
- --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
- --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
+mccabe==0.7.0
# via pylint
-more-itertools==8.14.0 \
- --hash=sha256:1bc4f91ee5b1b31ac7ceacc17c09befe6a40a503907baf9c839c229b5095cfd2 \
- --hash=sha256:c09443cd3d5438b8dafccd867a6bc1cb0894389e90cb53d227456b0b0bccb750
+mdurl==0.1.2
+ # via markdown-it-py
+more-itertools==9.1.0
# via jaraco-classes
-packaging==21.3 \
- --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
- --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
+packaging==23.1
# via
- # -r requirements/pytest.pip
# build
# pudb
+ # pyproject-api
# pytest
# sphinx
# tox
-parso==0.8.3 \
- --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \
- --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75
+parso==0.8.3
# via jedi
-pep517==0.13.0 \
- --hash=sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b \
- --hash=sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59
- # via build
-pkginfo==1.8.3 \
- --hash=sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594 \
- --hash=sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c
+pkginfo==1.9.6
# via twine
-platformdirs==2.5.2 \
- --hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
- --hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19
+platformdirs==3.5.1
# via
- # -r requirements/pip.pip
# pylint
+ # tox
# virtualenv
-pluggy==1.0.0 \
- --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \
- --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3
+pluggy==1.0.0
# via
- # -r requirements/pytest.pip
# pytest
# tox
-pudb==2022.1.2 \
- --hash=sha256:6b83ab805bddb53710109690a2237e98bf83c0b3a00033c517cdf5f6a8fa470d
+pudb==2022.1.3
# via -r requirements/dev.in
-py==1.11.0 \
- --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \
- --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378
- # via
- # -r requirements/pytest.pip
- # pytest
- # pytest-forked
- # tox
-pycontracts @ https://github.com/slorg1/contracts/archive/c5a6da27d4dc9985f68e574d20d86000880919c3.zip \
- --hash=sha256:2b889cbfb03b43dc811b5879248ac5c7e209ece78f03be9633de76a6b21a5a89
- # via -r requirements/pytest.pip
-pyenchant==3.2.2 \
- --hash=sha256:1cf830c6614362a78aab78d50eaf7c6c93831369c52e1bb64ffae1df0341e637 \
- --hash=sha256:5a636832987eaf26efe971968f4d1b78e81f62bca2bde0a9da210c7de43c3bce \
- --hash=sha256:5facc821ece957208a81423af7d6ec7810dad29697cb0d77aae81e4e11c8e5a6 \
- --hash=sha256:6153f521852e23a5add923dbacfbf4bebbb8d70c4e4bad609a8e0f9faeb915d1
+pyenchant==3.2.2
# via
# -r doc/requirements.in
# sphinxcontrib-spelling
-pygments==2.13.0 \
- --hash=sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1 \
- --hash=sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42
+pygments==2.15.1
# via
# pudb
# readme-renderer
# rich
# sphinx
-pylint==2.15.3 \
- --hash=sha256:5fdfd44af182866999e6123139d265334267339f29961f00c89783155eacc60b \
- --hash=sha256:7f6aad1d8d50807f7bc64f89ac75256a9baf8e6ed491cc9bc65592bc3f462cf1
+pylint==2.17.4
# via -r requirements/dev.in
-pyparsing==3.0.9 \
- --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
- --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
- # via
- # -r requirements/pytest.pip
- # packaging
- # pycontracts
-pytest==7.1.3 \
- --hash=sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7 \
- --hash=sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39
- # via
- # -r requirements/pytest.pip
- # pytest-forked
- # pytest-xdist
-pytest-forked==1.4.0 \
- --hash=sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e \
- --hash=sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8
+pyproject-api==1.5.1
+ # via tox
+pyproject-hooks==1.0.0
+ # via build
+pytest==7.3.1
# via
- # -r requirements/pytest.pip
+ # -r requirements/pytest.in
# pytest-xdist
-pytest-xdist==2.5.0 \
- --hash=sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf \
- --hash=sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65
- # via -r requirements/pytest.pip
-pytz==2022.2.1 \
- --hash=sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197 \
- --hash=sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5
+pytest-xdist==3.3.1
+ # via -r requirements/pytest.in
+pytz==2023.3
# via babel
-qualname==0.1.0 \
- --hash=sha256:277cf6aa4b2ad36beed1153cfa7bf521b210d54fbecb3d8eea0c5679cecc9ed8
- # via
- # -r requirements/pytest.pip
- # pycontracts
-readme-renderer==37.2 \
- --hash=sha256:d3f06a69e8c40fca9ab3174eca48f96d9771eddb43517b17d96583418427b106 \
- --hash=sha256:e8ad25293c98f781dbc2c5a36a309929390009f902f99e1798c761aaf04a7923
+readme-renderer==37.3
# via
# -r requirements/dev.in
# twine
-requests==2.28.1 \
- --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \
- --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349
+requests==2.31.0
# via
# -r requirements/dev.in
# requests-toolbelt
+ # scriv
# sphinx
# twine
-requests-toolbelt==0.9.1 \
- --hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f \
- --hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0
+requests-toolbelt==1.0.0
# via twine
-rfc3986==2.0.0 \
- --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \
- --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c
+rfc3986==2.0.0
# via twine
-rich==12.5.1 \
- --hash=sha256:2eb4e6894cde1e017976d2975ac210ef515d7548bc595ba20e195fb9628acdeb \
- --hash=sha256:63a5c5ce3673d3d5fbbf23cd87e11ab84b6b451436f1b7f19ec54b6bc36ed7ca
+rich==13.3.5
# via twine
-six==1.16.0 \
- --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
- --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+scriv==1.3.1
+ # via -r doc/requirements.in
+six==1.16.0
# via
- # -r requirements/pytest.pip
# bleach
- # libsass
# livereload
- # pycontracts
- # tox
-snowballstemmer==2.2.0 \
- --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
- --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
+snowballstemmer==2.2.0
# via sphinx
-sortedcontainers==2.4.0 \
- --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \
- --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0
- # via
- # -r requirements/pytest.pip
- # hypothesis
-sphinx==5.2.1 \
- --hash=sha256:3dcf00fcf82cf91118db9b7177edea4fc01998976f893928d0ab0c58c54be2ca \
- --hash=sha256:c009bb2e9ac5db487bcf53f015504005a330ff7c631bb6ab2604e0d65bae8b54
+sortedcontainers==2.4.0
+ # via hypothesis
+sphinx==5.3.0
# via
# -r doc/requirements.in
# sphinx-autobuild
# sphinx-rtd-theme
+ # sphinxcontrib-jquery
# sphinxcontrib-restbuilder
# sphinxcontrib-spelling
-sphinx-autobuild==2021.3.14 \
- --hash=sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac \
- --hash=sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05
+sphinx-autobuild==2021.3.14
# via -r doc/requirements.in
-sphinx-rtd-theme==1.0.0 \
- --hash=sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8 \
- --hash=sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c
+sphinx-rtd-theme==1.2.1
# via -r doc/requirements.in
-sphinxcontrib-applehelp==1.0.2 \
- --hash=sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a \
- --hash=sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58
+sphinxcontrib-applehelp==1.0.2
# via sphinx
-sphinxcontrib-devhelp==1.0.2 \
- --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \
- --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4
+sphinxcontrib-devhelp==1.0.2
# via sphinx
-sphinxcontrib-htmlhelp==2.0.0 \
- --hash=sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07 \
- --hash=sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2
+sphinxcontrib-htmlhelp==2.0.0
# via sphinx
-sphinxcontrib-jsmath==1.0.1 \
- --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \
- --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8
+sphinxcontrib-jquery==4.1
+ # via sphinx-rtd-theme
+sphinxcontrib-jsmath==1.0.1
# via sphinx
-sphinxcontrib-qthelp==1.0.3 \
- --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \
- --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6
+sphinxcontrib-qthelp==1.0.3
# via sphinx
-sphinxcontrib-restbuilder==0.3 \
- --hash=sha256:6b3ee9394b5ec5e73e6afb34d223530d0b9098cb7562f9c5e364e6d6b41410ce \
- --hash=sha256:6ba2ddc7a87d845c075c1b2e00d541bd1c8400488e50e32c9b4169ccdd9f30cb
+sphinxcontrib-restbuilder==0.3
# via -r doc/requirements.in
-sphinxcontrib-serializinghtml==1.1.5 \
- --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \
- --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952
+sphinxcontrib-serializinghtml==1.1.5
# via sphinx
-sphinxcontrib-spelling==7.6.0 \
- --hash=sha256:292cd7e1f73a763451693b4d48c9bded151084f6a91e5337733e9fa8715d20ec \
- --hash=sha256:6c1313618412511109f7b76029fbd60df5aa4acf67a2dc9cd1b1016d15e882ff
+sphinxcontrib-spelling==8.0.0
# via -r doc/requirements.in
-tomli==2.0.1 \
- --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
- --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+tomli==2.0.1
# via
- # -r requirements/pytest.pip
# build
# check-manifest
- # pep517
# pylint
+ # pyproject-api
+ # pyproject-hooks
# pytest
# tox
-tomlkit==0.11.4 \
- --hash=sha256:25d4e2e446c453be6360c67ddfb88838cfc42026322770ba13d1fbd403a93a5c \
- --hash=sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83
+tomlkit==0.11.8
# via pylint
-tornado==6.2 \
- --hash=sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca \
- --hash=sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72 \
- --hash=sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23 \
- --hash=sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8 \
- --hash=sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b \
- --hash=sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9 \
- --hash=sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13 \
- --hash=sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75 \
- --hash=sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac \
- --hash=sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e \
- --hash=sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b
+tornado==6.2
# via livereload
-tox==3.26.0 \
- --hash=sha256:44f3c347c68c2c68799d7d44f1808f9d396fc8a1a500cbc624253375c7ae107e \
- --hash=sha256:bf037662d7c740d15c9924ba23bb3e587df20598697bb985ac2b49bdc2d847f6
- # via -r requirements/dev.in
-twine==4.0.1 \
- --hash=sha256:42026c18e394eac3e06693ee52010baa5313e4811d5a11050e7d48436cf41b9e \
- --hash=sha256:96b1cf12f7ae611a4a40b6ae8e9570215daff0611828f5fe1f37a16255ab24a0
+tox==4.5.2
+ # via
+ # -r requirements/tox.in
+ # tox-gh
+tox-gh==1.0.0
+ # via -r requirements/tox.in
+twine==4.0.2
# via -r requirements/dev.in
-typed-ast==1.5.4 \
- --hash=sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2 \
- --hash=sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1 \
- --hash=sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6 \
- --hash=sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62 \
- --hash=sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac \
- --hash=sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d \
- --hash=sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc \
- --hash=sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2 \
- --hash=sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97 \
- --hash=sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35 \
- --hash=sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6 \
- --hash=sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1 \
- --hash=sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4 \
- --hash=sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c \
- --hash=sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e \
- --hash=sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec \
- --hash=sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f \
- --hash=sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72 \
- --hash=sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47 \
- --hash=sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72 \
- --hash=sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe \
- --hash=sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6 \
- --hash=sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3 \
- --hash=sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66
+typed-ast==1.5.4
# via astroid
-typing-extensions==4.3.0 \
- --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \
- --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6
+typing-extensions==4.6.2
# via
- # -r requirements/pip.pip
- # -r requirements/pytest.pip
# astroid
# importlib-metadata
+ # markdown-it-py
+ # platformdirs
# pylint
# rich
-urllib3==1.26.12 \
- --hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \
- --hash=sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997
+ # tox
+urllib3==2.0.2
# via
# requests
# twine
-urwid==2.1.2 \
- --hash=sha256:588bee9c1cb208d0906a9f73c613d2bd32c3ed3702012f51efe318a3f2127eae
+urwid==2.1.2
# via
# pudb
# urwid-readline
-urwid-readline==0.13 \
- --hash=sha256:018020cbc864bb5ed87be17dc26b069eae2755cb29f3a9c569aac3bded1efaf4
+urwid-readline==0.13
# via pudb
-virtualenv==20.16.5 \
- --hash=sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da \
- --hash=sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27
+virtualenv==20.23.0
# via
- # -r requirements/pip.pip
+ # -r requirements/pip.in
# tox
-webencodings==0.5.1 \
- --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
- --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
+webencodings==0.5.1
# via bleach
-wrapt==1.14.1 \
- --hash=sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3 \
- --hash=sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b \
- --hash=sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4 \
- --hash=sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2 \
- --hash=sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656 \
- --hash=sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3 \
- --hash=sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff \
- --hash=sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310 \
- --hash=sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a \
- --hash=sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57 \
- --hash=sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069 \
- --hash=sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383 \
- --hash=sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe \
- --hash=sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87 \
- --hash=sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d \
- --hash=sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b \
- --hash=sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907 \
- --hash=sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f \
- --hash=sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0 \
- --hash=sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28 \
- --hash=sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1 \
- --hash=sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853 \
- --hash=sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc \
- --hash=sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3 \
- --hash=sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3 \
- --hash=sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164 \
- --hash=sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1 \
- --hash=sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c \
- --hash=sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1 \
- --hash=sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7 \
- --hash=sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1 \
- --hash=sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320 \
- --hash=sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed \
- --hash=sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1 \
- --hash=sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248 \
- --hash=sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c \
- --hash=sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456 \
- --hash=sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77 \
- --hash=sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef \
- --hash=sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1 \
- --hash=sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7 \
- --hash=sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86 \
- --hash=sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4 \
- --hash=sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d \
- --hash=sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d \
- --hash=sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8 \
- --hash=sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5 \
- --hash=sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471 \
- --hash=sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00 \
- --hash=sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68 \
- --hash=sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3 \
- --hash=sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d \
- --hash=sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735 \
- --hash=sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d \
- --hash=sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569 \
- --hash=sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7 \
- --hash=sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59 \
- --hash=sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5 \
- --hash=sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb \
- --hash=sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b \
- --hash=sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f \
- --hash=sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462 \
- --hash=sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015 \
- --hash=sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af
+wrapt==1.15.0
# via astroid
-zipp==3.8.1 \
- --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \
- --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009
+zipp==3.15.0
# via
- # -r requirements/pip.pip
- # -r requirements/pytest.pip
# importlib-metadata
- # pep517
+ # importlib-resources
# The following packages are considered to be unsafe in a requirements file:
-pip==22.0.4 \
- --hash=sha256:b3a9de2c6ef801e9247d1527a4b16f92f2cc141cd1489f3fffaf6a9e96729764 \
- --hash=sha256:c6aca0f2f081363f689f041d90dab2a07a9a07fb840284db2218117a52da800b
- # via
- # -c doc/../requirements/pins.pip
- # -c requirements/pins.pip
- # -r requirements/pip.pip
-setuptools==65.4.0 \
- --hash=sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9 \
- --hash=sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1
- # via check-manifest
+pip==23.1.2
+ # via -r requirements/pip.in
+setuptools==67.8.0
+ # via
+ # -r requirements/pip.in
+ # check-manifest
diff -Nru python-coverage-6.5.0+dfsg1/requirements/mypy.in python-coverage-7.2.7+dfsg1/requirements/mypy.in
--- python-coverage-6.5.0+dfsg1/requirements/mypy.in 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/requirements/mypy.in 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +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
+
+-c pins.pip
+
+# So that we have pytest types.
+-r pytest.in
+
+mypy
diff -Nru python-coverage-6.5.0+dfsg1/requirements/mypy.pip python-coverage-7.2.7+dfsg1/requirements/mypy.pip
--- python-coverage-6.5.0+dfsg1/requirements/mypy.pip 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/requirements/mypy.pip 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +1,55 @@
+#
+# This file is autogenerated by pip-compile with Python 3.7
+# by the following command:
+#
+# make upgrade
+#
+attrs==23.1.0
+ # via hypothesis
+colorama==0.4.6
+ # via -r requirements/pytest.in
+exceptiongroup==1.1.1
+ # via
+ # hypothesis
+ # pytest
+execnet==1.9.0
+ # via pytest-xdist
+flaky==3.7.0
+ # via -r requirements/pytest.in
+hypothesis==6.75.6
+ # via -r requirements/pytest.in
+importlib-metadata==6.6.0
+ # via
+ # attrs
+ # pluggy
+ # pytest
+iniconfig==2.0.0
+ # via pytest
+mypy==1.3.0
+ # via -r requirements/mypy.in
+mypy-extensions==1.0.0
+ # via mypy
+packaging==23.1
+ # via pytest
+pluggy==1.0.0
+ # via pytest
+pytest==7.3.1
+ # via
+ # -r requirements/pytest.in
+ # pytest-xdist
+pytest-xdist==3.3.1
+ # via -r requirements/pytest.in
+sortedcontainers==2.4.0
+ # via hypothesis
+tomli==2.0.1
+ # via
+ # mypy
+ # pytest
+typed-ast==1.5.4
+ # via mypy
+typing-extensions==4.6.2
+ # via
+ # importlib-metadata
+ # mypy
+zipp==3.15.0
+ # via importlib-metadata
diff -Nru python-coverage-6.5.0+dfsg1/requirements/pins.pip python-coverage-7.2.7+dfsg1/requirements/pins.pip
--- python-coverage-6.5.0+dfsg1/requirements/pins.pip 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/requirements/pins.pip 2023-05-29 19:46:30.000000000 +0000
@@ -7,10 +7,8 @@
# but have different pins. This seems to satisfy them all:
#docutils>=0.17,<0.18
-# https://github.com/jazzband/pip-tools/issues/1617
-pip<22.1
-
-# requests gets different versions in dev.pip and doc/requirements.pip, not
-# sure why, and they then ask for different versions of certifi, and we can't
-# install, so pin certifi.
-certifi==2022.5.18.1
+# Setuptools became stricter about version number syntax. But it shouldn't be
+# checking the Python version like that, should it?
+# https://github.com/pypa/packaging/issues/678
+# https://github.com/nedbat/coveragepy/issues/1556
+#setuptools<66.0.0
diff -Nru python-coverage-6.5.0+dfsg1/requirements/pip.in python-coverage-7.2.7+dfsg1/requirements/pip.in
--- python-coverage-6.5.0+dfsg1/requirements/pip.in 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/requirements/pip.in 2023-05-29 19:46:30.000000000 +0000
@@ -6,4 +6,5 @@
# "make upgrade" turns this into requirements/pip.pip.
pip
+setuptools
virtualenv
diff -Nru python-coverage-6.5.0+dfsg1/requirements/pip.pip python-coverage-7.2.7+dfsg1/requirements/pip.pip
--- python-coverage-6.5.0+dfsg1/requirements/pip.pip 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/requirements/pip.pip 2023-05-29 19:46:30.000000000 +0000
@@ -1,42 +1,28 @@
#
-# This file is autogenerated by pip-compile with python 3.7
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.7
+# by the following command:
#
# make upgrade
#
-distlib==0.3.6 \
- --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \
- --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e
+distlib==0.3.6
# via virtualenv
-filelock==3.8.0 \
- --hash=sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc \
- --hash=sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4
+filelock==3.12.0
# via virtualenv
-importlib-metadata==4.12.0 \
- --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \
- --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23
+importlib-metadata==6.6.0
# via virtualenv
-platformdirs==2.5.2 \
- --hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
- --hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19
+platformdirs==3.5.1
# via virtualenv
-typing-extensions==4.3.0 \
- --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \
- --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6
- # via importlib-metadata
-virtualenv==20.16.5 \
- --hash=sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da \
- --hash=sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27
+typing-extensions==4.6.2
+ # via
+ # importlib-metadata
+ # platformdirs
+virtualenv==20.23.0
# via -r requirements/pip.in
-zipp==3.8.1 \
- --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \
- --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009
+zipp==3.15.0
# via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
-pip==22.0.4 \
- --hash=sha256:b3a9de2c6ef801e9247d1527a4b16f92f2cc141cd1489f3fffaf6a9e96729764 \
- --hash=sha256:c6aca0f2f081363f689f041d90dab2a07a9a07fb840284db2218117a52da800b
- # via
- # -c requirements/pins.pip
- # -r requirements/pip.in
+pip==23.1.2
+ # via -r requirements/pip.in
+setuptools==67.8.0
+ # via -r requirements/pip.in
diff -Nru python-coverage-6.5.0+dfsg1/requirements/pip-tools.pip python-coverage-7.2.7+dfsg1/requirements/pip-tools.pip
--- python-coverage-6.5.0+dfsg1/requirements/pip-tools.pip 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/requirements/pip-tools.pip 2023-05-29 19:46:30.000000000 +0000
@@ -1,69 +1,36 @@
#
-# This file is autogenerated by pip-compile with python 3.7
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.7
+# by the following command:
#
# make upgrade
#
-build==0.8.0 \
- --hash=sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437 \
- --hash=sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0
+build==0.10.0
# via pip-tools
-click==8.1.3 \
- --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \
- --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
+click==8.1.3
# via pip-tools
-importlib-metadata==4.12.0 \
- --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \
- --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23
+importlib-metadata==6.6.0
# via
# build
# click
- # pep517
-packaging==21.3 \
- --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
- --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
+packaging==23.1
# via build
-pep517==0.13.0 \
- --hash=sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b \
- --hash=sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59
- # via build
-pip-tools==6.8.0 \
- --hash=sha256:39e8aee465446e02278d80dbebd4325d1dd8633248f43213c73a25f58e7d8a55 \
- --hash=sha256:3e5cd4acbf383d19bdfdeab04738b6313ebf4ad22ce49bf529c729061eabfab8
+pip-tools==6.13.0
# via -r requirements/pip-tools.in
-pyparsing==3.0.9 \
- --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
- --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
- # via packaging
-tomli==2.0.1 \
- --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
- --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+pyproject-hooks==1.0.0
+ # via build
+tomli==2.0.1
# via
# build
- # pep517
-typing-extensions==4.3.0 \
- --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \
- --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6
+ # pyproject-hooks
+typing-extensions==4.6.2
# via importlib-metadata
-wheel==0.37.1 \
- --hash=sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a \
- --hash=sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4
+wheel==0.40.0
# via pip-tools
-zipp==3.8.1 \
- --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \
- --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009
- # via
- # importlib-metadata
- # pep517
+zipp==3.15.0
+ # via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
-pip==22.0.4 \
- --hash=sha256:b3a9de2c6ef801e9247d1527a4b16f92f2cc141cd1489f3fffaf6a9e96729764 \
- --hash=sha256:c6aca0f2f081363f689f041d90dab2a07a9a07fb840284db2218117a52da800b
- # via
- # -c requirements/pins.pip
- # pip-tools
-setuptools==65.4.0 \
- --hash=sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9 \
- --hash=sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1
+pip==23.1.2
+ # via pip-tools
+setuptools==67.8.0
# via pip-tools
diff -Nru python-coverage-6.5.0+dfsg1/requirements/pytest.in python-coverage-7.2.7+dfsg1/requirements/pytest.in
--- python-coverage-6.5.0+dfsg1/requirements/pytest.in 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/requirements/pytest.in 2023-05-29 19:46:30.000000000 +0000
@@ -10,19 +10,9 @@
hypothesis
pytest
pytest-xdist
-# Use a fork of PyContracts that supports Python 3.9
-#PyContracts==1.8.12
-# git+https://github.com/slorg1/contracts@collections_and_validator
-https://github.com/slorg1/contracts/archive/c5a6da27d4dc9985f68e574d20d86000880919c3.zip
# Pytest has a windows-only dependency on colorama:
# https://github.com/pytest-dev/pytest/blob/main/setup.cfg#L49
# colorama;sys_platform=="win32"
# We copy it here so it can get pinned.
colorama
-
-# Pytest has a windows-only dependency on atomicwrites:
-# https://github.com/pytest-dev/pytest/blob/7.1.2/setup.cfg#L50
-# atomicwrites>=1.0;sys_platform=="win32"
-# though it's been removed on main.
-atomicwrites>=1.0
diff -Nru python-coverage-6.5.0+dfsg1/requirements/pytest.pip python-coverage-7.2.7+dfsg1/requirements/pytest.pip
--- python-coverage-6.5.0+dfsg1/requirements/pytest.pip 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/requirements/pytest.pip 2023-05-29 19:46:30.000000000 +0000
@@ -1,117 +1,45 @@
#
-# This file is autogenerated by pip-compile with python 3.7
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.7
+# by the following command:
#
# make upgrade
#
-atomicwrites==1.4.1 \
- --hash=sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11
+attrs==23.1.0
+ # via hypothesis
+colorama==0.4.6
# via -r requirements/pytest.in
-attrs==22.1.0 \
- --hash=sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6 \
- --hash=sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c
+exceptiongroup==1.1.1
# via
# hypothesis
# pytest
-backports-functools-lru-cache==1.6.4 \
- --hash=sha256:d5ed2169378b67d3c545e5600d363a923b09c456dab1593914935a68ad478271 \
- --hash=sha256:dbead04b9daa817909ec64e8d2855fb78feafe0b901d4568758e3a60559d8978
- # via pycontracts
-colorama==0.4.5 \
- --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \
- --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4
- # via -r requirements/pytest.in
-decorator==5.1.1 \
- --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \
- --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186
- # via pycontracts
-exceptiongroup==1.0.0rc9 \
- --hash=sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337 \
- --hash=sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96
- # via hypothesis
-execnet==1.9.0 \
- --hash=sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5 \
- --hash=sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142
+execnet==1.9.0
# via pytest-xdist
-flaky==3.7.0 \
- --hash=sha256:3ad100780721a1911f57a165809b7ea265a7863305acb66708220820caf8aa0d \
- --hash=sha256:d6eda73cab5ae7364504b7c44670f70abed9e75f77dd116352f662817592ec9c
+flaky==3.7.0
# via -r requirements/pytest.in
-future==0.18.2 \
- --hash=sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d
- # via pycontracts
-hypothesis==6.54.6 \
- --hash=sha256:2d5e2d5ccd0efce4e0968a6164f4e4853f808e33f4d91490c975c98beec0c7c3 \
- --hash=sha256:e44833325f9a55f795596ceefd7ede7d626cfe45836025d2647cccaff7070e10
+hypothesis==6.75.6
# via -r requirements/pytest.in
-importlib-metadata==4.12.0 \
- --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \
- --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23
+importlib-metadata==6.6.0
# via
+ # attrs
# pluggy
# pytest
-iniconfig==1.1.1 \
- --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \
- --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32
+iniconfig==2.0.0
# via pytest
-packaging==21.3 \
- --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
- --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
+packaging==23.1
# via pytest
-pluggy==1.0.0 \
- --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \
- --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3
+pluggy==1.0.0
# via pytest
-py==1.11.0 \
- --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \
- --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378
- # via
- # pytest
- # pytest-forked
-pycontracts @ https://github.com/slorg1/contracts/archive/c5a6da27d4dc9985f68e574d20d86000880919c3.zip \
- --hash=sha256:2b889cbfb03b43dc811b5879248ac5c7e209ece78f03be9633de76a6b21a5a89
- # via -r requirements/pytest.in
-pyparsing==3.0.9 \
- --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
- --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
- # via
- # packaging
- # pycontracts
-pytest==7.1.3 \
- --hash=sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7 \
- --hash=sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39
+pytest==7.3.1
# via
# -r requirements/pytest.in
- # pytest-forked
# pytest-xdist
-pytest-forked==1.4.0 \
- --hash=sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e \
- --hash=sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8
- # via pytest-xdist
-pytest-xdist==2.5.0 \
- --hash=sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf \
- --hash=sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65
+pytest-xdist==3.3.1
# via -r requirements/pytest.in
-qualname==0.1.0 \
- --hash=sha256:277cf6aa4b2ad36beed1153cfa7bf521b210d54fbecb3d8eea0c5679cecc9ed8
- # via pycontracts
-six==1.16.0 \
- --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
- --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
- # via pycontracts
-sortedcontainers==2.4.0 \
- --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \
- --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0
+sortedcontainers==2.4.0
# via hypothesis
-tomli==2.0.1 \
- --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
- --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+tomli==2.0.1
# via pytest
-typing-extensions==4.3.0 \
- --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \
- --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6
+typing-extensions==4.6.2
# via importlib-metadata
-zipp==3.8.1 \
- --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \
- --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009
+zipp==3.15.0
# via importlib-metadata
diff -Nru python-coverage-6.5.0+dfsg1/requirements/tox.in python-coverage-7.2.7+dfsg1/requirements/tox.in
--- python-coverage-6.5.0+dfsg1/requirements/tox.in 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/requirements/tox.in 2023-05-29 19:46:30.000000000 +0000
@@ -7,7 +7,7 @@
# "make upgrade" turns this into requirements/tox.pip.
tox
-tox-gh-actions
+tox-gh
# Tox has a windows-only dependency on colorama:
# https://github.com/tox-dev/tox/blob/master/setup.cfg#L44
diff -Nru python-coverage-6.5.0+dfsg1/requirements/tox.pip python-coverage-7.2.7+dfsg1/requirements/tox.pip
--- python-coverage-6.5.0+dfsg1/requirements/tox.pip 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/requirements/tox.pip 2023-05-29 19:46:30.000000000 +0000
@@ -1,83 +1,56 @@
#
-# This file is autogenerated by pip-compile with python 3.7
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.7
+# by the following command:
#
# make upgrade
#
-colorama==0.4.5 \
- --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \
- --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4
- # via -r requirements/tox.in
-distlib==0.3.6 \
- --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \
- --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e
+cachetools==5.3.1
+ # via tox
+chardet==5.1.0
+ # via tox
+colorama==0.4.6
+ # via
+ # -r requirements/tox.in
+ # tox
+distlib==0.3.6
# via virtualenv
-filelock==3.8.0 \
- --hash=sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc \
- --hash=sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4
+filelock==3.12.0
# via
# tox
# virtualenv
-importlib-metadata==4.12.0 \
- --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \
- --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23
+importlib-metadata==6.6.0
# via
# pluggy
# tox
# virtualenv
-importlib-resources==5.9.0 \
- --hash=sha256:5481e97fb45af8dcf2f798952625591c58fe599d0735d86b10f54de086a61681 \
- --hash=sha256:f78a8df21a79bcc30cfd400bdc38f314333de7c0fb619763f6b9dabab8268bb7
- # via tox-gh-actions
-packaging==21.3 \
- --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
- --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
- # via tox
-platformdirs==2.5.2 \
- --hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
- --hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19
- # via virtualenv
-pluggy==1.0.0 \
- --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \
- --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3
- # via tox
-py==1.11.0 \
- --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \
- --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378
- # via tox
-pyparsing==3.0.9 \
- --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
- --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
- # via packaging
-six==1.16.0 \
- --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
- --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+packaging==23.1
+ # via
+ # pyproject-api
+ # tox
+platformdirs==3.5.1
+ # via
+ # tox
+ # virtualenv
+pluggy==1.0.0
# via tox
-tomli==2.0.1 \
- --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
- --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+pyproject-api==1.5.1
# via tox
-tox==3.26.0 \
- --hash=sha256:44f3c347c68c2c68799d7d44f1808f9d396fc8a1a500cbc624253375c7ae107e \
- --hash=sha256:bf037662d7c740d15c9924ba23bb3e587df20598697bb985ac2b49bdc2d847f6
+tomli==2.0.1
+ # via
+ # pyproject-api
+ # tox
+tox==4.5.2
# via
# -r requirements/tox.in
- # tox-gh-actions
-tox-gh-actions==2.10.0 \
- --hash=sha256:4a21e70d799736016cf75c3415f5991008cf81fa6ee1c047d1be24900cec31bb \
- --hash=sha256:acb64641f09022581040d92c28d6c06d261a829c008575892fd458e23e638971
+ # tox-gh
+tox-gh==1.0.0
# via -r requirements/tox.in
-typing-extensions==4.3.0 \
- --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \
- --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6
- # via importlib-metadata
-virtualenv==20.16.5 \
- --hash=sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da \
- --hash=sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27
- # via tox
-zipp==3.8.1 \
- --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \
- --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009
+typing-extensions==4.6.2
# via
# importlib-metadata
- # importlib-resources
+ # platformdirs
+ # tox
+virtualenv==20.23.0
+ # via tox
+zipp==3.15.0
+ # via importlib-metadata
diff -Nru python-coverage-6.5.0+dfsg1/setup.cfg python-coverage-7.2.7+dfsg1/setup.cfg
--- python-coverage-6.5.0+dfsg1/setup.cfg 2022-09-29 16:36:48.202740000 +0000
+++ python-coverage-7.2.7+dfsg1/setup.cfg 2023-05-29 19:46:41.742986000 +0000
@@ -1,25 +1,3 @@
-[tool:pytest]
-addopts = -q -n auto --strict-markers --no-flaky-report -rfEX --failed-first
-python_classes = *Test
-markers =
- expensive: too slow to run during "make smoke"
-filterwarnings =
- ignore:the imp module is deprecated in favour of importlib:DeprecationWarning
- ignore:distutils Version classes are deprecated:DeprecationWarning
- ignore:The distutils package is deprecated and slated for removal in Python 3.12:DeprecationWarning
-xfail_strict = true
-balanced_clumps =
- VirtualenvTest
- CompareTest
- GetZipBytesTest
-
-[pep8]
-ignore = E265,E266,E123,E133,E226,E241,E242,E301,E401
-max-line-length = 100
-
-[metadata]
-license_files = LICENSE.txt
-
[egg_info]
tag_build =
tag_date = 0
diff -Nru python-coverage-6.5.0+dfsg1/setup.py python-coverage-7.2.7+dfsg1/setup.py
--- python-coverage-6.5.0+dfsg1/setup.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/setup.py 2023-05-29 19:46:30.000000000 +0000
@@ -14,20 +14,6 @@
from distutils.core import Extension # pylint: disable=wrong-import-order
from setuptools.command.build_ext import build_ext # pylint: disable=wrong-import-order
from distutils import errors # pylint: disable=wrong-import-order
-import distutils.log # pylint: disable=wrong-import-order
-
-# $set_env.py: COVERAGE_QUIETER - Set to remove some noise from test output.
-if bool(int(os.getenv("COVERAGE_QUIETER", "0"))):
- # Distutils has its own mini-logging code, and it sets the level too high.
- # When I ask for --quiet when running tests, I don't want to see warnings.
- old_set_verbosity = distutils.log.set_verbosity
- def better_set_verbosity(v):
- """--quiet means no warnings!"""
- if v <= 0:
- distutils.log.set_threshold(distutils.log.ERROR)
- else:
- old_set_verbosity(v)
- distutils.log.set_verbosity = better_set_verbosity
# Get or massage our metadata. We exec coverage/version.py so we can avoid
# importing the product code into setup.py.
@@ -45,6 +31,7 @@
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
+Programming Language :: Python :: 3.12
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: PyPy
Topic :: Software Development :: Quality Assurance
@@ -58,10 +45,19 @@
# Keep pylint happy.
__version__ = __url__ = version_info = ""
# Execute the code in version.py.
- exec(compile(version_file.read(), cov_ver_py, 'exec'))
+ exec(compile(version_file.read(), cov_ver_py, 'exec', dont_inherit=True))
with open("README.rst") as readme:
- long_description = readme.read().replace("https://coverage.readthedocs.io", __url__)
+ readme_text = readme.read()
+
+temp_url = __url__.replace("readthedocs", "@@")
+assert "@@" not in readme_text
+long_description = (
+ readme_text
+ .replace("https://coverage.readthedocs.io/en/latest", temp_url)
+ .replace("https://coverage.readthedocs.io", temp_url)
+ .replace("@@", "readthedocs")
+)
with open("CONTRIBUTORS.txt", "rb") as contributors:
paras = contributors.read().split(b"\n\n")
@@ -93,6 +89,7 @@
'coverage': [
'htmlfiles/*.*',
'fullcoverage/*.*',
+ 'py.typed',
]
},
@@ -120,7 +117,8 @@
long_description=long_description,
long_description_content_type='text/x-rst',
keywords='code coverage testing',
- license='Apache 2.0',
+ license='Apache-2.0',
+ license_files=["LICENSE.txt"],
classifiers=classifier_list,
url="https://github.com/nedbat/coveragepy",
project_urls={
@@ -130,7 +128,8 @@
'?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=pypi'
),
'Issues': 'https://github.com/nedbat/coveragepy/issues',
- 'Twitter': 'https://twitter.com/coveragepy',
+ 'Mastodon': 'https://hachyderm.io/@coveragepy',
+ 'Mastodon (nedbat)': 'https://hachyderm.io/@nedbat',
},
python_requires=">=3.7", # minimum of PYVERSIONS
)
@@ -185,10 +184,6 @@
compile_extension = True
-if sys.platform.startswith('java'):
- # Jython can't compile C extensions
- compile_extension = False
-
if '__pypy__' in sys.builtin_module_names:
# Pypy can't compile C extensions
compile_extension = False
diff -Nru python-coverage-6.5.0+dfsg1/tests/balance_xdist_plugin.py python-coverage-7.2.7+dfsg1/tests/balance_xdist_plugin.py
--- python-coverage-6.5.0+dfsg1/tests/balance_xdist_plugin.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/balance_xdist_plugin.py 2023-05-29 19:46:30.000000000 +0000
@@ -29,6 +29,7 @@
import os
import shutil
import time
+
from pathlib import Path
import pytest
@@ -64,7 +65,7 @@
if not self.running_all:
return
- tests_csv_dir = Path(session.startdir).resolve() / "tmp/tests_csv"
+ tests_csv_dir = session.startpath.resolve() / "tmp/tests_csv"
self.tests_csv = tests_csv_dir / f"{self.worker}.csv"
if self.worker == "none":
@@ -104,7 +105,7 @@
yield
self.write_duration_row(item, "teardown", time.time() - start)
- @pytest.mark.trylast
+ @pytest.hookimpl(trylast=True)
def pytest_xdist_make_scheduler(self, config, log):
"""Create our BalancedScheduler using time data from the last run."""
# Assign tests to chunks
diff -Nru python-coverage-6.5.0+dfsg1/tests/conftest.py python-coverage-7.2.7+dfsg1/tests/conftest.py
--- python-coverage-6.5.0+dfsg1/tests/conftest.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/conftest.py 2023-05-29 19:46:30.000000000 +0000
@@ -7,16 +7,19 @@
This module is run automatically by pytest, to define and enable fixtures.
"""
+from __future__ import annotations
+
import os
import sys
import sysconfig
import warnings
+
from pathlib import Path
+from typing import Iterator, Optional
import pytest
from coverage import env
-from coverage.exceptions import _StopEverything
from coverage.files import set_relative_directory
# Pytest will rewrite assertions in test modules, but not elsewhere.
@@ -31,13 +34,13 @@
@pytest.fixture(autouse=True)
-def set_warnings():
+def set_warnings() -> None:
"""Configure warnings to show while running tests."""
warnings.simplefilter("default")
warnings.simplefilter("once", DeprecationWarning)
# Warnings to suppress:
- # How come these warnings are successfully suppressed here, but not in setup.cfg??
+ # How come these warnings are successfully suppressed here, but not in pyproject.toml??
warnings.filterwarnings(
"ignore",
@@ -62,7 +65,7 @@
@pytest.fixture(autouse=True)
-def reset_sys_path():
+def reset_sys_path() -> Iterator[None]:
"""Clean up sys.path changes around every test."""
sys_path = list(sys.path)
yield
@@ -70,7 +73,7 @@
@pytest.fixture(autouse=True)
-def reset_environment():
+def reset_environment() -> Iterator[None]:
"""Make sure a test setting an envvar doesn't leak into another test."""
old_environ = os.environ.copy()
yield
@@ -79,14 +82,14 @@
@pytest.fixture(autouse=True)
-def reset_filesdotpy_globals():
+def reset_filesdotpy_globals() -> Iterator[None]:
"""coverage/files.py has some unfortunate globals. Reset them every test."""
set_relative_directory()
yield
WORKER = os.environ.get("PYTEST_XDIST_WORKER", "none")
-def pytest_sessionstart():
+def pytest_sessionstart() -> None:
"""Run once at the start of the test session."""
# Only in the main process...
if WORKER == "none":
@@ -97,7 +100,7 @@
# subcover.pth is deleted by pytest_sessionfinish below.
-def pytest_sessionfinish():
+def pytest_sessionfinish() -> None:
"""Hook the end of a test session, to clean up."""
# This is called by each of the workers and by the main process.
if WORKER == "none":
@@ -106,16 +109,8 @@
if pth_file.exists():
pth_file.unlink()
-@pytest.hookimpl(hookwrapper=True)
-def pytest_runtest_call(item):
- """Run once for each test."""
- # Convert _StopEverything into skipped tests.
- outcome = yield
- if outcome.excinfo and issubclass(outcome.excinfo[0], _StopEverything): # pragma: only jython
- pytest.skip(f"Skipping {item.nodeid} for _StopEverything: {outcome.excinfo[1]}")
-
-def possible_pth_dirs():
+def possible_pth_dirs() -> Iterator[Path]:
"""Produce a sequence of directories for trying to write .pth files."""
# First look through sys.path, and if we find a .pth file, then it's a good
# place to put ours.
@@ -129,7 +124,7 @@
yield Path(sysconfig.get_path("purelib")) # pragma: cant happen
-def find_writable_pth_directory():
+def find_writable_pth_directory() -> Optional[Path]:
"""Find a place to write a .pth file."""
for pth_dir in possible_pth_dirs(): # pragma: part covered
try_it = pth_dir / f"touch_{WORKER}.it"
diff -Nru python-coverage-6.5.0+dfsg1/tests/coveragetest.py python-coverage-7.2.7+dfsg1/tests/coveragetest.py
--- python-coverage-6.5.0+dfsg1/tests/coveragetest.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/coveragetest.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,8 @@
"""Base test case class for coverage.py testing."""
+from __future__ import annotations
+
import contextlib
import datetime
import difflib
@@ -15,12 +17,18 @@
import shlex
import sys
-import pytest
+from types import ModuleType
+from typing import (
+ Any, Collection, Dict, Iterable, Iterator, List, Mapping, Optional,
+ Sequence, Tuple, Union,
+)
import coverage
-from coverage import env
+from coverage import Coverage
from coverage.cmdline import CoverageScript
+from coverage.data import CoverageData
from coverage.misc import import_local_file
+from coverage.types import TArc, TLineNo
from tests.helpers import arcs_to_arcz_repr, arcz_to_arcs, assert_count_equal
from tests.helpers import nice_file, run_command
@@ -56,15 +64,20 @@
# Let stderr go to stderr, pytest will capture it for us.
show_stderr = True
- def setUp(self):
+ def setUp(self) -> None:
super().setUp()
# Attributes for getting info about what happened.
- self.last_command_status = None
- self.last_command_output = None
- self.last_module_name = None
-
- def start_import_stop(self, cov, modname, modfile=None):
+ self.last_command_status: Optional[int] = None
+ self.last_command_output: Optional[str] = None
+ self.last_module_name: Optional[str] = None
+
+ def start_import_stop(
+ self,
+ cov: Coverage,
+ modname: str,
+ modfile: Optional[str] = None
+ ) -> ModuleType:
"""Start coverage, import a file, then stop coverage.
`cov` is started and stopped, with an `import_local_file` of
@@ -83,22 +96,28 @@
cov.stop()
return mod
- def get_report(self, cov, squeeze=True, **kwargs):
+ def get_report(self, cov: Coverage, squeeze: bool = True, **kwargs: Any) -> str:
"""Get the report from `cov`, and canonicalize it."""
repout = io.StringIO()
kwargs.setdefault("show_missing", False)
cov.report(file=repout, **kwargs)
report = repout.getvalue().replace('\\', '/')
+ print(report) # When tests fail, it's helpful to see the output
if squeeze:
report = re.sub(r" +", " ", report)
return report
- def get_module_name(self):
+ def get_module_name(self) -> str:
"""Return a random module name to use for this test run."""
self.last_module_name = 'coverage_test_' + str(random.random())[2:]
return self.last_module_name
- def _check_arcs(self, a1, a2, arc_type):
+ def _check_arcs(
+ self,
+ a1: Optional[Iterable[TArc]],
+ a2: Optional[Iterable[TArc]],
+ arc_type: str,
+ ) -> str:
"""Check that the arc lists `a1` and `a2` are equal.
If they are equal, return empty string. If they are unequal, return
@@ -116,11 +135,20 @@
return ""
def check_coverage(
- self, text, lines=None, missing="", report="",
- excludes=None, partials="",
- arcz=None, arcz_missing=None, arcz_unpredicted=None,
- arcs=None, arcs_missing=None, arcs_unpredicted=None,
- ):
+ self,
+ text: str,
+ lines: Optional[Union[Sequence[TLineNo], Sequence[List[TLineNo]]]] = None,
+ missing: Union[str, Sequence[str]] = "",
+ report: str = "",
+ excludes: Optional[Iterable[str]] = None,
+ partials: Iterable[str] = (),
+ arcz: Optional[str] = None,
+ arcz_missing: Optional[str] = None,
+ arcz_unpredicted: Optional[str] = None,
+ arcs: Optional[Iterable[TArc]] = None,
+ arcs_missing: Optional[Iterable[TArc]] = None,
+ arcs_unpredicted: Optional[Iterable[TArc]] = None,
+ ) -> Coverage:
"""Check the coverage measurement of `text`.
The source `text` is run and measured. `lines` are the line numbers
@@ -174,7 +202,7 @@
if isinstance(lines[0], int):
# lines is just a list of numbers, it must match the statements
# found in the code.
- assert statements == lines, f"{statements!r} != {lines!r}"
+ assert statements == lines, f"lines: {statements!r} != {lines!r}"
else:
# lines is a list of possible line number lists, one of them
# must match.
@@ -186,7 +214,7 @@
missing_formatted = analysis.missing_formatted()
if isinstance(missing, str):
- msg = f"{missing_formatted!r} != {missing!r}"
+ msg = f"missing: {missing_formatted!r} != {missing!r}"
assert missing_formatted == missing, msg
else:
for missing_list in missing:
@@ -218,18 +246,33 @@
return cov
- def make_data_file(self, basename=None, suffix=None, lines=None, file_tracers=None):
+ def make_data_file(
+ self,
+ basename: Optional[str] = None,
+ suffix: Optional[str] = None,
+ lines: Optional[Mapping[str, Collection[TLineNo]]] = None,
+ arcs: Optional[Mapping[str, Collection[TArc]]] = None,
+ file_tracers: Optional[Mapping[str, str]] = None,
+ ) -> CoverageData:
"""Write some data into a coverage data file."""
data = coverage.CoverageData(basename=basename, suffix=suffix)
+ assert lines is None or arcs is None
if lines:
data.add_lines(lines)
+ if arcs:
+ data.add_arcs(arcs)
if file_tracers:
data.add_file_tracers(file_tracers)
data.write()
return data
@contextlib.contextmanager
- def assert_warnings(self, cov, warnings, not_warnings=()):
+ def assert_warnings(
+ self,
+ cov: Coverage,
+ warnings: Iterable[str],
+ not_warnings: Iterable[str] = (),
+ ) -> Iterator[None]:
"""A context manager to check that particular warnings happened in `cov`.
`cov` is a Coverage instance. `warnings` is a list of regexes. Every
@@ -247,7 +290,11 @@
"""
__tracebackhide__ = True
saved_warnings = []
- def capture_warning(msg, slug=None, once=False): # pylint: disable=unused-argument
+ def capture_warning(
+ msg: str,
+ slug: Optional[str] = None,
+ once: bool = False, # pylint: disable=unused-argument
+ ) -> None:
"""A fake implementation of Coverage._warn, to capture warnings."""
# NOTE: we don't implement `once`.
if slug:
@@ -255,7 +302,7 @@
saved_warnings.append(msg)
original_warn = cov._warn
- cov._warn = capture_warning
+ cov._warn = capture_warning # type: ignore[method-assign]
try:
yield
@@ -280,36 +327,41 @@
if saved_warnings:
assert False, f"Unexpected warnings: {saved_warnings!r}"
finally:
- cov._warn = original_warn
+ cov._warn = original_warn # type: ignore[method-assign]
- def assert_same_files(self, flist1, flist2):
+ def assert_same_files(self, flist1: Iterable[str], flist2: Iterable[str]) -> None:
"""Assert that `flist1` and `flist2` are the same set of file names."""
flist1_nice = [nice_file(f) for f in flist1]
flist2_nice = [nice_file(f) for f in flist2]
assert_count_equal(flist1_nice, flist2_nice)
- def assert_exists(self, fname):
+ def assert_exists(self, fname: str) -> None:
"""Assert that `fname` is a file that exists."""
assert os.path.exists(fname), f"File {fname!r} should exist"
- def assert_doesnt_exist(self, fname):
+ def assert_doesnt_exist(self, fname: str) -> None:
"""Assert that `fname` is a file that doesn't exist."""
assert not os.path.exists(fname), f"File {fname!r} shouldn't exist"
- def assert_file_count(self, pattern, count):
+ def assert_file_count(self, pattern: str, count: int) -> None:
"""Assert that there are `count` files matching `pattern`."""
files = sorted(glob.glob(pattern))
msg = "There should be {} files matching {!r}, but there are these: {}"
msg = msg.format(count, pattern, files)
assert len(files) == count, msg
- def assert_recent_datetime(self, dt, seconds=10, msg=None):
+ def assert_recent_datetime(
+ self,
+ dt: datetime.datetime,
+ seconds: int = 10,
+ msg: Optional[str] = None,
+ ) -> None:
"""Assert that `dt` marks a time at most `seconds` seconds ago."""
age = datetime.datetime.now() - dt
assert age.total_seconds() >= 0, msg
assert age.total_seconds() <= seconds, msg
- def command_line(self, args, ret=OK):
+ def command_line(self, args: str, ret: int = OK) -> None:
"""Run `args` through the command line.
Use this when you want to run the full coverage machinery, but in the
@@ -330,7 +382,7 @@
# https://salsa.debian.org/debian/pkg-python-coverage/-/blob/master/debian/patches/02.rename-public-programs.patch
coverage_command = "coverage"
- def run_command(self, cmd):
+ def run_command(self, cmd: str) -> str:
"""Run the command-line `cmd` in a sub-process.
`cmd` is the command line to invoke in a sub-process. Returns the
@@ -347,7 +399,7 @@
_, output = self.run_command_status(cmd)
return output
- def run_command_status(self, cmd):
+ def run_command_status(self, cmd: str) -> Tuple[int, str]:
"""Run the command-line `cmd` in a sub-process, and print its output.
Use this when you need to test the process behavior of coverage.
@@ -382,18 +434,9 @@
command_words = [os.path.basename(sys.executable)]
elif command_name == "coverage":
- if env.JYTHON: # pragma: only jython
- # Jython can't do reporting, so let's skip the test now.
- if command_args and command_args[0] in ('report', 'html', 'xml', 'annotate'):
- pytest.skip("Can't run reporting commands in Jython")
- # Jython can't run "coverage" as a command because the shebang
- # refers to another shebang'd Python script. So run them as
- # modules.
- command_words = "jython -m coverage".split()
- else:
- # The invocation requests the coverage.py program. Substitute the
- # actual coverage.py main command name.
- command_words = [self.coverage_command]
+ # The invocation requests the coverage.py program. Substitute the
+ # actual coverage.py main command name.
+ command_words = [self.coverage_command]
else:
command_words = [command_name]
@@ -403,8 +446,6 @@
# Add our test modules directory to PYTHONPATH. I'm sure there's too
# much path munging here, but...
pythonpath_name = "PYTHONPATH"
- if env.JYTHON:
- pythonpath_name = "JYTHONPATH" # pragma: only jython
testmods = nice_file(self.working_root(), "tests/modules")
zipfile = nice_file(self.working_root(), "tests/zipmods.zip")
@@ -418,36 +459,36 @@
print(self.last_command_output)
return self.last_command_status, self.last_command_output
- def working_root(self):
+ def working_root(self) -> str:
"""Where is the root of the coverage.py working tree?"""
return os.path.dirname(nice_file(__file__, ".."))
- def report_from_command(self, cmd):
+ def report_from_command(self, cmd: str) -> str:
"""Return the report from the `cmd`, with some convenience added."""
report = self.run_command(cmd).replace('\\', '/')
assert "error" not in report.lower()
return report
- def report_lines(self, report):
+ def report_lines(self, report: str) -> List[str]:
"""Return the lines of the report, as a list."""
lines = report.split('\n')
assert lines[-1] == ""
return lines[:-1]
- def line_count(self, report):
+ def line_count(self, report: str) -> int:
"""How many lines are in `report`?"""
return len(self.report_lines(report))
- def squeezed_lines(self, report):
+ def squeezed_lines(self, report: str) -> List[str]:
"""Return a list of the lines in report, with the spaces squeezed."""
lines = self.report_lines(report)
return [re.sub(r"\s+", " ", l.strip()) for l in lines]
- def last_line_squeezed(self, report):
+ def last_line_squeezed(self, report: str) -> str:
"""Return the last line of `report` with the spaces squeezed down."""
return self.squeezed_lines(report)[-1]
- def get_measured_filenames(self, coverage_data):
+ def get_measured_filenames(self, coverage_data: CoverageData) -> Dict[str, str]:
"""Get paths to measured files.
Returns a dict of {filename: absolute path to file}
@@ -456,9 +497,10 @@
return {os.path.basename(filename): filename
for filename in coverage_data.measured_files()}
- def get_missing_arc_description(self, cov, start, end):
+ def get_missing_arc_description(self, cov: Coverage, start: TLineNo, end: TLineNo) -> str:
"""Get the missing-arc description for a line arc in a coverage run."""
# ugh, unexposed methods??
+ assert self.last_module_name is not None
filename = self.last_module_name + ".py"
fr = cov._get_file_reporter(filename)
arcs_executed = cov._analyze(filename).arcs_executed()
@@ -468,8 +510,8 @@
class UsingModulesMixin:
"""A mixin for importing modules from tests/modules and tests/moremodules."""
- def setUp(self):
- super().setUp()
+ def setUp(self) -> None:
+ super().setUp() # type: ignore[misc]
# Parent class saves and restores sys.path, we can just modify it.
sys.path.append(nice_file(TESTS_DIR, "modules"))
@@ -477,7 +519,7 @@
sys.path.append(nice_file(TESTS_DIR, "zipmods.zip"))
-def command_line(args):
+def command_line(args: str) -> int:
"""Run `args` through the CoverageScript command line.
Returns the return code from CoverageScript.command_line.
diff -Nru python-coverage-6.5.0+dfsg1/tests/gold/html/contexts/index.html python-coverage-7.2.7+dfsg1/tests/gold/html/contexts/index.html
--- python-coverage-6.5.0+dfsg1/tests/gold/html/contexts/index.html 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/gold/html/contexts/index.html 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +1,102 @@
+
+
+
+
+ Coverage report
+
+
+
+
+
+
+
+ Coverage report:
+ 94%
+
+
+
+
+ coverage.py v7.2.3a0.dev1,
+ created at 2023-03-21 08:44 -0400
+
+
+
+
+
+
+
+ Module
+ statements
+ missing
+ excluded
+ coverage
+
+
+
+
+ two_tests.py
+ 17
+ 1
+ 0
+ 94%
+
+
+
+
+ Total
+ 17
+ 1
+ 0
+ 94%
+
+
+
+
+ No items found using the specified filter.
+
+
+
+
+
diff -Nru python-coverage-6.5.0+dfsg1/tests/gold/html/contexts/two_tests_py.html python-coverage-7.2.7+dfsg1/tests/gold/html/contexts/two_tests_py.html
--- python-coverage-6.5.0+dfsg1/tests/gold/html/contexts/two_tests_py.html 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/gold/html/contexts/two_tests_py.html 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +1,126 @@
+
+
+
+
+ Coverage for two_tests.py: 94%
+
+
+
+
+
+
+
+
+
+ Coverage for two_tests.py:
+ 94%
+
+
+
+ 17 statements
+
+
+
+
+
+ « prev
+ ^ index
+ » next
+
+ coverage.py v7.2.3a0.dev1,
+ created at 2023-04-01 08:30 -0400
+
+
+
+
+
+ 1def helper(lineno):
+ 2 x = 2 1acb
+
+ 4def test_one():
+ 5 a = 5 1c
+ 6 helper(6) 1c
+
+ 8def test_two():
+ 9 a = 9 1b
+ 10 b = 10 1b
+ 11 if a > 11: 1b
+ 12 b = 12
+ 13 assert a == (13-4) 1b
+ 14 assert b == (14-4) 1b
+ 15 helper( 1b
+ 16 16
+ 17 )
+
+ 19test_one()
+ 20x = 20
+ 21helper(21)
+ 22test_two()
+
+
+
+
diff -Nru python-coverage-6.5.0+dfsg1/tests/gold/html/Makefile python-coverage-7.2.7+dfsg1/tests/gold/html/Makefile
--- python-coverage-6.5.0+dfsg1/tests/gold/html/Makefile 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/gold/html/Makefile 2023-05-29 19:46:30.000000000 +0000
@@ -17,7 +17,7 @@
clean: ## Remove the effects of this Makefile.
@git clean -fq .
-update-gold: ## Copy output files from latest tests to gold files.
+update-gold: ## Copy actual output files from latest tests to gold files.
@for sub in ../../actual/html/*; do \
rsync --verbose --existing --recursive $$sub/ $$(basename $$sub) ; \
done ; \
diff -Nru python-coverage-6.5.0+dfsg1/tests/gold/html/styled/style.css python-coverage-7.2.7+dfsg1/tests/gold/html/styled/style.css
--- python-coverage-6.5.0+dfsg1/tests/gold/html/styled/style.css 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/gold/html/styled/style.css 2023-05-29 19:46:30.000000000 +0000
@@ -258,12 +258,10 @@
@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } }
-#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; }
+#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; text-align: right; }
@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } }
-#source p .ctxs span { display: block; text-align: right; }
-
#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; }
#index table.index { margin-left: -.5em; }
diff -Nru python-coverage-6.5.0+dfsg1/tests/gold/html/support/coverage_html.js python-coverage-7.2.7+dfsg1/tests/gold/html/support/coverage_html.js
--- python-coverage-6.5.0+dfsg1/tests/gold/html/support/coverage_html.js 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/gold/html/support/coverage_html.js 2023-05-29 19:46:30.000000000 +0000
@@ -166,7 +166,7 @@
// Trigger change event on setup, to force filter on page refresh
// (filter value may still be present).
- document.getElementById("filter").dispatchEvent(new Event("change"));
+ document.getElementById("filter").dispatchEvent(new Event("input"));
};
coverage.INDEX_SORT_STORAGE = "COVERAGE_INDEX_SORT_2";
@@ -214,7 +214,7 @@
coverage.pyfile_ready = function () {
// If we're directed to a particular line number, highlight the line.
var frag = location.hash;
- if (frag.length > 2 && frag[1] === 't') {
+ if (frag.length > 2 && frag[1] === "t") {
document.querySelector(frag).closest(".n").classList.add("highlight");
coverage.set_sel(parseInt(frag.substr(2), 10));
} else {
@@ -257,6 +257,10 @@
coverage.init_scroll_markers();
coverage.wire_up_sticky_header();
+ document.querySelectorAll("[id^=ctxs]").forEach(
+ cbox => cbox.addEventListener("click", coverage.expand_contexts)
+ );
+
// Rebuild scroll markers when the window height changes.
window.addEventListener("resize", coverage.build_scroll_markers);
};
@@ -528,14 +532,14 @@
coverage.init_scroll_markers = function () {
// Init some variables
- coverage.lines_len = document.querySelectorAll('#source > p').length;
+ coverage.lines_len = document.querySelectorAll("#source > p").length;
// Build html
coverage.build_scroll_markers();
};
coverage.build_scroll_markers = function () {
- const temp_scroll_marker = document.getElementById('scroll_marker')
+ const temp_scroll_marker = document.getElementById("scroll_marker")
if (temp_scroll_marker) temp_scroll_marker.remove();
// Don't build markers if the window has no scroll bar.
if (document.body.scrollHeight <= window.innerHeight) {
@@ -549,11 +553,11 @@
const scroll_marker = document.createElement("div");
scroll_marker.id = "scroll_marker";
- document.getElementById('source').querySelectorAll(
- 'p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par'
+ document.getElementById("source").querySelectorAll(
+ "p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par"
).forEach(element => {
const line_top = Math.floor(element.offsetTop * marker_scale);
- const line_number = parseInt(element.id.substr(1));
+ const line_number = parseInt(element.querySelector(".n a").id.substr(1));
if (line_number === previous_line + 1) {
// If this solid missed block just make previous mark higher.
@@ -577,24 +581,40 @@
};
coverage.wire_up_sticky_header = function () {
- const header = document.querySelector('header');
+ const header = document.querySelector("header");
const header_bottom = (
- header.querySelector('.content h2').getBoundingClientRect().top -
+ header.querySelector(".content h2").getBoundingClientRect().top -
header.getBoundingClientRect().top
);
function updateHeader() {
if (window.scrollY > header_bottom) {
- header.classList.add('sticky');
+ header.classList.add("sticky");
} else {
- header.classList.remove('sticky');
+ header.classList.remove("sticky");
}
}
- window.addEventListener('scroll', updateHeader);
+ window.addEventListener("scroll", updateHeader);
updateHeader();
};
+coverage.expand_contexts = function (e) {
+ var ctxs = e.target.parentNode.querySelector(".ctxs");
+
+ if (!ctxs.classList.contains("expanded")) {
+ var ctxs_text = ctxs.textContent;
+ var width = Number(ctxs_text[0]);
+ ctxs.textContent = "";
+ for (var i = 1; i < ctxs_text.length; i += width) {
+ key = ctxs_text.substring(i, i + width).trim();
+ ctxs.appendChild(document.createTextNode(contexts[key]));
+ ctxs.appendChild(document.createElement("br"));
+ }
+ ctxs.classList.add("expanded");
+ }
+};
+
document.addEventListener("DOMContentLoaded", () => {
if (document.body.classList.contains("indexfile")) {
coverage.index_ready();
diff -Nru python-coverage-6.5.0+dfsg1/tests/gold/html/support/style.css python-coverage-7.2.7+dfsg1/tests/gold/html/support/style.css
--- python-coverage-6.5.0+dfsg1/tests/gold/html/support/style.css 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/gold/html/support/style.css 2023-05-29 19:46:30.000000000 +0000
@@ -258,12 +258,10 @@
@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } }
-#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; }
+#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; text-align: right; }
@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } }
-#source p .ctxs span { display: block; text-align: right; }
-
#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; }
#index table.index { margin-left: -.5em; }
diff -Nru python-coverage-6.5.0+dfsg1/tests/gold/README.rst python-coverage-7.2.7+dfsg1/tests/gold/README.rst
--- python-coverage-6.5.0+dfsg1/tests/gold/README.rst 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/gold/README.rst 2023-05-29 19:46:30.000000000 +0000
@@ -9,16 +9,40 @@
If gold tests are failing, you may need to update the gold files by copying the
current output of the tests into the gold files. When a test fails, the actual
-output is in the tests/actual directory. Do not commit those files to git.
+output is in the tests/actual directory. Those files are ignored by git.
-You can run just the failed tests again with::
+There's a Makefile in the html directory for working with gold files and their
+associated support files.
+
+To view the tests/actual files, you need to tentatively copy them to the gold
+directories, and then add the supporting files so they can be viewed as
+complete output. For example::
+
+ cp tests/actual/html/contexts/* tests/gold/html/contexts
+ cd tests/actual/html
+ make complete
+
+If the new actual output is correct, you can use "make update-gold" to copy the
+actual output as the new gold files.
+
+If you have changed some of the supporting files (.css or .js), then "make
+update-support" will copy the updated files to the tests/gold/html/support
+directory for checking test output.
+
+If you have added a gold test, you'll need to manually copy the tests/actual
+files to tests/gold.
+
+Once you've copied the actual results to the gold files, or to check your work
+again, you can run just the failed tests again with::
tox -e py39 -- -n 0 --lf
The saved HTML files in the html directories can't be viewed properly without
the supporting CSS and Javascript files. But we don't want to save copies of
-those files in every subdirectory. There's a Makefile in the html directory
-for working with the saved copies of the support files.
+those files in every subdirectory. The make target "make complete" in
+tests/gold/html will copy the support files so you can open the HTML files to
+see how they look. When you are done checking the output, you can use "make
+clean" to remove the support files from the gold directories.
If the output files are correct, you can update the gold files with "make
update-gold". If there are version-specific gold files (for example,
diff -Nru python-coverage-6.5.0+dfsg1/tests/goldtest.py python-coverage-7.2.7+dfsg1/tests/goldtest.py
--- python-coverage-6.5.0+dfsg1/tests/goldtest.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/goldtest.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,8 @@
"""A test base class for tests based on gold file comparison."""
+from __future__ import annotations
+
import difflib
import filecmp
import fnmatch
@@ -11,19 +13,24 @@
import re
import xml.etree.ElementTree
+from typing import Iterable, List, Optional, Tuple
+
from tests.coveragetest import TESTS_DIR
from tests.helpers import os_sep
-def gold_path(path):
+def gold_path(path: str) -> str:
"""Get a path to a gold file for comparison."""
return os.path.join(TESTS_DIR, "gold", path)
def compare(
- expected_dir, actual_dir, file_pattern=None,
- actual_extra=False, scrubs=None,
- ):
+ expected_dir: str,
+ actual_dir: str,
+ file_pattern: Optional[str] = None,
+ actual_extra: bool = False,
+ scrubs: Optional[List[Tuple[str, str]]] = None,
+) -> None:
"""Compare files matching `file_pattern` in `expected_dir` and `actual_dir`.
`actual_extra` true means `actual_dir` can have extra files in it
@@ -39,19 +46,23 @@
"""
__tracebackhide__ = True # pytest, please don't show me this function.
assert os_sep("/gold/") in expected_dir
+ assert os.path.exists(actual_dir)
+ os.makedirs(expected_dir, exist_ok=True)
dc = filecmp.dircmp(expected_dir, actual_dir)
- diff_files = fnmatch_list(dc.diff_files, file_pattern)
- expected_only = fnmatch_list(dc.left_only, file_pattern)
- actual_only = fnmatch_list(dc.right_only, file_pattern)
+ diff_files = _fnmatch_list(dc.diff_files, file_pattern)
+ expected_only = _fnmatch_list(dc.left_only, file_pattern)
+ actual_only = _fnmatch_list(dc.right_only, file_pattern)
- def save_mismatch(f):
+ def save_mismatch(f: str) -> None:
"""Save a mismatched result to tests/actual."""
save_path = expected_dir.replace(os_sep("/gold/"), os_sep("/actual/"))
os.makedirs(save_path, exist_ok=True)
- with open(os.path.join(save_path, f), "w") as savef:
+ save_file = os.path.join(save_path, f)
+ with open(save_file, "w") as savef:
with open(os.path.join(actual_dir, f)) as readf:
savef.write(readf.read())
+ print(os_sep(f"Saved actual output to '{save_file}': see tests/gold/README.rst"))
# filecmp only compares in binary mode, but we want text mode. So
# look through the list of different files, and compare them
@@ -75,10 +86,10 @@
actual = scrub(actual, scrubs)
if expected != actual:
text_diff.append(f'{expected_file} != {actual_file}')
- expected = expected.splitlines()
- actual = actual.splitlines()
+ expected_lines = expected.splitlines()
+ actual_lines = actual.splitlines()
print(f":::: diff '{expected_file}' and '{actual_file}'")
- print("\n".join(difflib.Differ().compare(expected, actual)))
+ print("\n".join(difflib.Differ().compare(expected_lines, actual_lines)))
print(f":::: end diff '{expected_file}' and '{actual_file}'")
save_mismatch(f)
@@ -93,7 +104,7 @@
assert not actual_only, f"Files in {actual_dir} only: {actual_only}"
-def contains(filename, *strlist):
+def contains(filename: str, *strlist: str) -> None:
"""Check that the file contains all of a list of strings.
An assert will be raised if one of the arguments in `strlist` is
@@ -107,7 +118,7 @@
assert s in text, f"Missing content in {filename}: {s!r}"
-def contains_rx(filename, *rxlist):
+def contains_rx(filename: str, *rxlist: str) -> None:
"""Check that the file has lines that re.search all of the regexes.
An assert will be raised if one of the regexes in `rxlist` doesn't match
@@ -123,7 +134,7 @@
)
-def contains_any(filename, *strlist):
+def contains_any(filename: str, *strlist: str) -> None:
"""Check that the file contains at least one of a list of strings.
An assert will be raised if none of the arguments in `strlist` is in
@@ -140,7 +151,7 @@
assert False, f"Missing content in {filename}: {strlist[0]!r} [1 of {len(strlist)}]"
-def doesnt_contain(filename, *strlist):
+def doesnt_contain(filename: str, *strlist: str) -> None:
"""Check that the file contains none of a list of strings.
An assert will be raised if any of the strings in `strlist` appears in
@@ -156,16 +167,15 @@
# Helpers
-def canonicalize_xml(xtext):
+def canonicalize_xml(xtext: str) -> str:
"""Canonicalize some XML text."""
root = xml.etree.ElementTree.fromstring(xtext)
for node in root.iter():
node.attrib = dict(sorted(node.items()))
- xtext = xml.etree.ElementTree.tostring(root)
- return xtext.decode("utf-8")
+ return xml.etree.ElementTree.tostring(root).decode("utf-8")
-def fnmatch_list(files, file_pattern):
+def _fnmatch_list(files: List[str], file_pattern: Optional[str]) -> List[str]:
"""Filter the list of `files` to only those that match `file_pattern`.
If `file_pattern` is None, then return the entire list of files.
Returns a list of the filtered files.
@@ -175,7 +185,7 @@
return files
-def scrub(strdata, scrubs):
+def scrub(strdata: str, scrubs: Iterable[Tuple[str, str]]) -> str:
"""Scrub uninteresting data from the payload in `strdata`.
`scrubs` is a list of (find, replace) pairs of regexes that are used on
`strdata`. A string is returned.
diff -Nru python-coverage-6.5.0+dfsg1/tests/helpers.py python-coverage-7.2.7+dfsg1/tests/helpers.py
--- python-coverage-6.5.0+dfsg1/tests/helpers.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/helpers.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,8 @@
"""Helpers for coverage.py tests."""
+from __future__ import annotations
+
import collections
import contextlib
import os
@@ -13,16 +15,21 @@
import textwrap
import warnings
-from unittest import mock
+from pathlib import Path
+from typing import (
+ Any, Callable, Iterable, Iterator, List, Optional, Set, Tuple, Type,
+ TypeVar, Union, cast,
+)
import pytest
from coverage import env
from coverage.exceptions import CoverageWarning
from coverage.misc import output_encoding
+from coverage.types import TArc, TLineNo
-def run_command(cmd):
+def run_command(cmd: str) -> Tuple[int, str]:
"""Run a command in a sub-process.
Returns the exit status code and the combined stdout and stderr.
@@ -30,8 +37,8 @@
"""
# Subprocesses are expensive, but convenient, and so may be over-used in
# the test suite. Use these lines to get a list of the tests using them:
- if 0: # pragma: debugging
- with open("/tmp/processes.txt", "a") as proctxt:
+ if 0: # pragma: debugging
+ with open("/tmp/processes.txt", "a") as proctxt: # type: ignore[unreachable]
print(os.environ.get("PYTEST_CURRENT_TEST", "unknown"), file=proctxt, flush=True)
# In some strange cases (PyPy3 in a virtualenv!?) the stdout encoding of
@@ -46,24 +53,29 @@
env=sub_env,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT
+ stderr=subprocess.STDOUT,
)
output, _ = proc.communicate()
status = proc.returncode
# Get the output, and canonicalize it to strings with newlines.
- output = output.decode(output_encoding()).replace("\r", "")
- return status, output
+ output_str = output.decode(output_encoding()).replace("\r", "")
+ return status, output_str
-def make_file(filename, text="", bytes=b"", newline=None):
+def make_file(
+ filename: str,
+ text: str = "",
+ bytes: bytes = b"",
+ newline: Optional[str] = None,
+) -> str:
"""Create a file for testing.
`filename` is the relative path to the file, including directories if
desired, which will be created if need be.
- `text` is the content to create in the file, a native string (bytes in
- Python 2, unicode in Python 3), or `bytes` are the bytes to write.
+ `text` is the text content to create in the file, or `bytes` are the
+ bytes to write.
If `newline` is provided, it is a string that will be used as the line
endings in the created file, otherwise the line endings are as provided
@@ -91,8 +103,8 @@
f.write(data)
# For debugging, enable this to show the contents of files created.
- if 0: # pragma: debugging
- print(f" ───┬──┤ {filename} ├───────────────────────")
+ if 0: # pragma: debugging
+ print(f" ───┬──┤ {filename} ├───────────────────────") # type: ignore[unreachable]
for lineno, line in enumerate(data.splitlines(), start=1):
print(f"{lineno:6}│ {line.rstrip().decode()}")
print()
@@ -100,25 +112,26 @@
return filename
-def nice_file(*fparts):
+def nice_file(*fparts: str) -> str:
"""Canonicalize the file name composed of the parts in `fparts`."""
fname = os.path.join(*fparts)
return os.path.normcase(os.path.abspath(os.path.realpath(fname)))
-def os_sep(s):
+def os_sep(s: str) -> str:
"""Replace slashes in `s` with the correct separator for the OS."""
return s.replace("/", os.sep)
class CheckUniqueFilenames:
"""Asserts the uniqueness of file names passed to a function."""
- def __init__(self, wrapped):
- self.filenames = set()
+
+ def __init__(self, wrapped: Callable[..., Any]) -> None:
+ self.filenames: Set[str] = set()
self.wrapped = wrapped
@classmethod
- def hook(cls, obj, method_name):
+ def hook(cls, obj: Any, method_name: str) -> CheckUniqueFilenames:
"""Replace a method with our checking wrapper.
The method must take a string as a first argument. That argument
@@ -133,17 +146,16 @@
setattr(obj, method_name, hook.wrapper)
return hook
- def wrapper(self, filename, *args, **kwargs):
+ def wrapper(self, filename: str, *args: Any, **kwargs: Any) -> Any:
"""The replacement method. Check that we don't have dupes."""
assert filename not in self.filenames, (
f"File name {filename!r} passed to {self.wrapped!r} twice"
)
self.filenames.add(filename)
- ret = self.wrapped(filename, *args, **kwargs)
- return ret
+ return self.wrapped(filename, *args, **kwargs)
-def re_lines(pat, text, match=True):
+def re_lines(pat: str, text: str, match: bool = True) -> List[str]:
"""Return a list of lines selected by `pat` in the string `text`.
If `match` is false, the selection is inverted: only the non-matching
@@ -156,12 +168,12 @@
return [l for l in text.splitlines() if bool(re.search(pat, l)) == match]
-def re_lines_text(pat, text, match=True):
+def re_lines_text(pat: str, text: str, match: bool = True) -> str:
"""Return the multi-line text of lines selected by `pat`."""
return "".join(l + "\n" for l in re_lines(pat, text, match=match))
-def re_line(pat, text):
+def re_line(pat: str, text: str) -> str:
"""Return the one line in `text` that matches regex `pat`.
Raises an AssertionError if more than one, or less than one, line matches.
@@ -172,7 +184,7 @@
return lines[0]
-def remove_tree(dirname):
+def remove_tree(dirname: str) -> None:
"""Remove a directory tree.
It's fine for the directory to not exist in the first place.
@@ -186,7 +198,8 @@
_arcz_map.update({c: ord(c) - ord('0') for c in '123456789'})
_arcz_map.update({c: 10 + ord(c) - ord('A') for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'})
-def arcz_to_arcs(arcz):
+
+def arcz_to_arcs(arcz: str) -> List[TArc]:
"""Convert a compact textual representation of arcs to a list of pairs.
The text has space-separated pairs of letters. Period is -1, 1-9 are
@@ -200,19 +213,23 @@
"-11, 12, 2-5" --> [(-1,1), (1,2), (2,-5)]
"""
+ # The `type: ignore[misc]` here are to suppress "Unpacking a string is
+ # disallowed".
+ a: str
+ b: str
arcs = []
for pair in arcz.split():
asgn = bsgn = 1
if len(pair) == 2:
- a, b = pair
+ a, b = pair # type: ignore[misc]
else:
assert len(pair) == 3
- if pair[0] == '-':
- _, a, b = pair
+ if pair[0] == "-":
+ _, a, b = pair # type: ignore[misc]
asgn = -1
else:
- assert pair[1] == '-'
- a, _, b = pair
+ assert pair[1] == "-"
+ a, _, b = pair # type: ignore[misc]
bsgn = -1
arcs.append((asgn * _arcz_map[a], bsgn * _arcz_map[b]))
return sorted(arcs)
@@ -220,7 +237,8 @@
_arcz_unmap = {val: ch for ch, val in _arcz_map.items()}
-def _arcs_to_arcz_repr_one(num):
+
+def _arcs_to_arcz_repr_one(num: TLineNo) -> str:
"""Return an arcz form of the number `num`, or "?" if there is none."""
if num == -1:
return "."
@@ -232,7 +250,7 @@
return z
-def arcs_to_arcz_repr(arcs):
+def arcs_to_arcz_repr(arcs: Optional[Iterable[TArc]]) -> str:
"""Convert a list of arcs to a readable multi-line form for asserting.
Each pair is on its own line, with a comment showing the arcz form,
@@ -250,7 +268,7 @@
@contextlib.contextmanager
-def change_dir(new_dir):
+def change_dir(new_dir: Union[str, Path]) -> Iterator[None]:
"""Change directory, and then change back.
Use as a context manager, it will return to the original
@@ -264,43 +282,37 @@
finally:
os.chdir(old_dir)
+T = TypeVar("T")
-def without_module(using_module, missing_module_name):
- """
- Hide a module for testing.
-
- Use this in a test function to make an optional module unavailable during
- the test::
-
- with without_module(product.something, 'tomli'):
- use_toml_somehow()
-
- Arguments:
- using_module: a module in which to hide `missing_module_name`.
- missing_module_name (str): the name of the module to hide.
-
- """
- return mock.patch.object(using_module, missing_module_name, None)
-
-
-def assert_count_equal(a, b):
+def assert_count_equal(
+ a: Optional[Iterable[T]],
+ b: Optional[Iterable[T]],
+) -> None:
"""
A pytest-friendly implementation of assertCountEqual.
Assert that `a` and `b` have the same elements, but maybe in different order.
This only works for hashable elements.
"""
+ assert a is not None
+ assert b is not None
assert collections.Counter(list(a)) == collections.Counter(list(b))
-def assert_coverage_warnings(warns, *msgs):
+def assert_coverage_warnings(
+ warns: Iterable[warnings.WarningMessage],
+ *msgs: Union[str, re.Pattern[str]],
+) -> None:
"""
Assert that the CoverageWarning's in `warns` have `msgs` as messages.
+
+ Each msg can be a string compared for equality, or a compiled regex used to
+ search the text.
"""
assert msgs # don't call this without some messages.
warns = [w for w in warns if issubclass(w.category, CoverageWarning)]
assert len(warns) == len(msgs)
- for actual, expected in zip((w.message.args[0] for w in warns), msgs):
+ for actual, expected in zip((cast(Warning, w.message).args[0] for w in warns), msgs):
if hasattr(expected, "search"):
assert expected.search(actual), f"{actual!r} didn't match {expected!r}"
else:
@@ -308,7 +320,10 @@
@contextlib.contextmanager
-def swallow_warnings(message=r".", category=CoverageWarning):
+def swallow_warnings(
+ message: str = r".",
+ category: Type[Warning] = CoverageWarning,
+) -> Iterator[None]:
"""Swallow particular warnings.
It's OK if they happen, or if they don't happen. Just ignore them.
@@ -318,7 +333,7 @@
yield
-xfail_pypy_3749 = pytest.mark.xfail(
- env.PYVERSION[:2] == (3, 8) and env.PYPY and env.PYPYVERSION >= (7, 3, 10),
- reason="Avoid a PyPy bug: https://foss.heptapod.net/pypy/pypy/-/issues/3749",
+xfail_pypy38 = pytest.mark.xfail(
+ env.PYPY and env.PYVERSION[:2] == (3, 8) and env.PYPYVERSION < (7, 3, 11),
+ reason="These tests fail on older PyPy 3.8",
)
diff -Nru python-coverage-6.5.0+dfsg1/tests/mixins.py python-coverage-7.2.7+dfsg1/tests/mixins.py
--- python-coverage-6.5.0+dfsg1/tests/mixins.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/mixins.py 2023-05-29 19:46:30.000000000 +0000
@@ -7,11 +7,15 @@
Some of these are transitional while working toward pure-pytest style.
"""
+from __future__ import annotations
+
import importlib
import os
import os.path
import sys
+from typing import Any, Callable, Iterable, Iterator, Optional, Tuple, cast
+
import pytest
from coverage.misc import SysModuleSaver
@@ -22,26 +26,30 @@
"""A base class to connect to pytest in a test class hierarchy."""
@pytest.fixture(autouse=True)
- def connect_to_pytest(self, request, monkeypatch):
+ def connect_to_pytest(
+ self,
+ request: pytest.FixtureRequest,
+ monkeypatch: pytest.MonkeyPatch,
+ ) -> None:
"""Captures pytest facilities for use by other test helpers."""
# pylint: disable=attribute-defined-outside-init
self._pytest_request = request
self._monkeypatch = monkeypatch
self.setUp()
- def setUp(self):
+ def setUp(self) -> None:
"""Per-test initialization. Override this as you wish."""
pass
- def addCleanup(self, fn, *args):
+ def addCleanup(self, fn: Callable[..., None], *args: Any) -> None:
"""Like unittest's addCleanup: code to call when the test is done."""
self._pytest_request.addfinalizer(lambda: fn(*args))
- def set_environ(self, name, value):
+ def set_environ(self, name: str, value: str) -> None:
"""Set an environment variable `name` to be `value`."""
self._monkeypatch.setenv(name, value)
- def del_environ(self, name):
+ def del_environ(self, name: str) -> None:
"""Delete an environment variable, unless we set it."""
self._monkeypatch.delenv(name, raising=False)
@@ -55,10 +63,10 @@
run_in_temp_dir = True
@pytest.fixture(autouse=True)
- def _temp_dir(self, tmpdir_factory):
+ def _temp_dir(self, tmp_path_factory: pytest.TempPathFactory) -> Iterator[None]:
"""Create a temp dir for the tests, if they want it."""
if self.run_in_temp_dir:
- tmpdir = tmpdir_factory.mktemp("t")
+ tmpdir = tmp_path_factory.mktemp("t")
self.temp_dir = str(tmpdir)
with change_dir(self.temp_dir):
# Modules should be importable from this temp directory. We don't
@@ -70,7 +78,13 @@
else:
yield
- def make_file(self, filename, text="", bytes=b"", newline=None):
+ def make_file(
+ self,
+ filename: str,
+ text: str = "",
+ bytes: bytes = b"",
+ newline: Optional[str] = None,
+ ) -> str:
"""Make a file. See `tests.helpers.make_file`"""
# pylint: disable=redefined-builtin # bytes
assert self.run_in_temp_dir, "Only use make_file when running in a temp dir"
@@ -81,7 +95,7 @@
"""Auto-restore the imported modules at the end of each test."""
@pytest.fixture(autouse=True)
- def _module_saving(self):
+ def _module_saving(self) -> Iterable[None]:
"""Remove modules we imported during the test."""
self._sys_module_saver = SysModuleSaver()
try:
@@ -89,7 +103,7 @@
finally:
self._sys_module_saver.restore()
- def clean_local_file_imports(self):
+ def clean_local_file_imports(self) -> None:
"""Clean up the results of calls to `import_local_file`.
Use this if you need to `import_local_file` the same file twice in
@@ -99,7 +113,7 @@
# So that we can re-import files, clean them out first.
self._sys_module_saver.restore()
- # Also have to clean out the .pyc files, since the timestamp
+ # Also have to clean out the .pyc files, since the time stamp
# resolution is only one second, a changed file might not be
# picked up.
remove_tree("__pycache__")
@@ -118,18 +132,18 @@
"""
@pytest.fixture(autouse=True)
- def _capcapsys(self, capsys):
+ def _capcapsys(self, capsys: pytest.CaptureFixture[str]) -> None:
"""Grab the fixture so our methods can use it."""
self.capsys = capsys
- def stdouterr(self):
+ def stdouterr(self) -> Tuple[str, str]:
"""Returns (out, err), two strings for stdout and stderr."""
- return self.capsys.readouterr()
+ return cast(Tuple[str, str], self.capsys.readouterr())
- def stdout(self):
+ def stdout(self) -> str:
"""Returns a string, the captured stdout."""
return self.capsys.readouterr().out
- def stderr(self):
+ def stderr(self) -> str:
"""Returns a string, the captured stderr."""
return self.capsys.readouterr().err
diff -Nru python-coverage-6.5.0+dfsg1/tests/modules/plugins/another.py python-coverage-7.2.7+dfsg1/tests/modules/plugins/another.py
--- python-coverage-6.5.0+dfsg1/tests/modules/plugins/another.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/modules/plugins/another.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,12 +3,19 @@
"""A plugin for tests to reference."""
-from coverage import CoveragePlugin
+from __future__ import annotations
+
+from typing import Any
+from coverage import CoveragePlugin
+from coverage.plugin_support import Plugins
class Plugin(CoveragePlugin):
pass
-def coverage_init(reg, options):
+def coverage_init(
+ reg: Plugins,
+ options: Any, # pylint: disable=unused-argument
+) -> None:
reg.add_file_tracer(Plugin())
diff -Nru python-coverage-6.5.0+dfsg1/tests/modules/plugins/a_plugin.py python-coverage-7.2.7+dfsg1/tests/modules/plugins/a_plugin.py
--- python-coverage-6.5.0+dfsg1/tests/modules/plugins/a_plugin.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/modules/plugins/a_plugin.py 2023-05-29 19:46:30.000000000 +0000
@@ -1,11 +1,19 @@
"""A plugin for tests to reference."""
+from __future__ import annotations
+
+from typing import Any
+
from coverage import CoveragePlugin
+from coverage.plugin_support import Plugins
class Plugin(CoveragePlugin):
pass
-def coverage_init(reg, options):
+def coverage_init(
+ reg: Plugins,
+ options: Any, # pylint: disable=unused-argument
+) -> None:
reg.add_file_tracer(Plugin())
diff -Nru python-coverage-6.5.0+dfsg1/tests/modules/process_test/try_execfile.py python-coverage-7.2.7+dfsg1/tests/modules/process_test/try_execfile.py
--- python-coverage-6.5.0+dfsg1/tests/modules/process_test/try_execfile.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/modules/process_test/try_execfile.py 2023-05-29 19:46:30.000000000 +0000
@@ -20,17 +20,21 @@
"""
+from __future__ import annotations
+
import itertools
import json
import os
import sys
+from typing import Any, List
+
# sys.path varies by execution environments. Coverage.py uses setuptools to
# make console scripts, which means pkg_resources is imported. pkg_resources
# removes duplicate entries from sys.path. So we do that too, since the extra
# entries don't affect the running of the program.
-def same_file(p1, p2):
+def same_file(p1: str, p2: str) -> bool:
"""Determine if `p1` and `p2` refer to the same existing file."""
if not p1:
return not p2
@@ -45,9 +49,9 @@
norm2 = os.path.normcase(os.path.normpath(p2))
return norm1 == norm2
-def without_same_files(filenames):
+def without_same_files(filenames: List[str]) -> List[str]:
"""Return the list `filenames` with duplicates (by same_file) removed."""
- reduced = []
+ reduced: List[str] = []
for filename in filenames:
if not any(same_file(filename, other) for other in reduced):
reduced.append(filename)
@@ -59,7 +63,7 @@
import __main__
-def my_function(a):
+def my_function(a: Any) -> str:
"""A function to force execution of module-level values."""
return f"my_fn({a!r})"
@@ -71,7 +75,7 @@
# A more compact ad-hoc grouped-by-first-letter list of builtins.
CLUMPS = "ABC,DEF,GHI,JKLMN,OPQR,ST,U,VWXYZ_,ab,cd,efg,hij,lmno,pqr,stuvwxyz".split(",")
-def word_group(w):
+def word_group(w: str) -> int:
"""Figure out which CLUMP the first letter of w is in."""
for i, clump in enumerate(CLUMPS):
if w[0] in clump:
diff -Nru python-coverage-6.5.0+dfsg1/tests/osinfo.py python-coverage-7.2.7+dfsg1/tests/osinfo.py
--- python-coverage-6.5.0+dfsg1/tests/osinfo.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/osinfo.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,12 +3,14 @@
"""OS information for testing."""
-from coverage import env
+from __future__ import annotations
+import sys
-if env.WINDOWS:
+
+if sys.platform == "win32":
# Windows implementation
- def process_ram():
+ def process_ram() -> int:
"""How much RAM is this process using? (Windows)"""
import ctypes
# From: http://lists.ubuntu.com/archives/bazaar-commits/2009-February/011990.html
@@ -38,35 +40,35 @@
return 0 # pragma: cant happen
return mem_struct.PrivateUsage
-elif env.LINUX:
+elif sys.platform.startswith("linux"):
# Linux implementation
import os
_scale = {'kb': 1024, 'mb': 1024*1024}
- def _VmB(key):
+ def _VmB(key: str) -> int:
"""Read the /proc/PID/status file to find memory use."""
try:
# Get pseudo file /proc//status
- with open('/proc/%d/status' % os.getpid()) as t:
+ with open(f"/proc/{os.getpid()}/status") as t:
v = t.read()
except OSError: # pragma: cant happen
return 0 # non-Linux?
# Get VmKey line e.g. 'VmRSS: 9999 kB\n ...'
i = v.index(key)
- v = v[i:].split(None, 3)
- if len(v) < 3: # pragma: part covered
+ vp = v[i:].split(None, 3)
+ if len(vp) < 3: # pragma: part covered
return 0 # pragma: cant happen
# Convert Vm value to bytes.
- return int(float(v[1]) * _scale[v[2].lower()])
+ return int(float(vp[1]) * _scale[vp[2].lower()])
- def process_ram():
+ def process_ram() -> int:
"""How much RAM is this process using? (Linux implementation)"""
return _VmB('VmRSS')
else:
# Generic implementation.
- def process_ram():
+ def process_ram() -> int:
"""How much RAM is this process using? (stdlib implementation)"""
import resource
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
diff -Nru python-coverage-6.5.0+dfsg1/tests/plugin1.py python-coverage-7.2.7+dfsg1/tests/plugin1.py
--- python-coverage-6.5.0+dfsg1/tests/plugin1.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/plugin1.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,28 +3,34 @@
"""A file tracer plugin for test_plugins.py to import."""
+from __future__ import annotations
+
import os.path
-import coverage
+from types import FrameType
+from typing import Any, Optional, Set, Tuple, Union
+from coverage import CoveragePlugin, FileReporter, FileTracer
+from coverage.plugin_support import Plugins
+from coverage.types import TLineNo
-class Plugin(coverage.CoveragePlugin):
+class Plugin(CoveragePlugin):
"""A file tracer plugin to import, so that it isn't in the test's current directory."""
- def file_tracer(self, filename):
+ def file_tracer(self, filename: str) -> Optional[FileTracer]:
"""Trace only files named xyz.py"""
if "xyz.py" in filename:
- return FileTracer(filename)
+ return MyFileTracer(filename)
return None
- def file_reporter(self, filename):
- return FileReporter(filename)
+ def file_reporter(self, filename: str) -> Union[FileReporter, str]:
+ return MyFileReporter(filename)
-class FileTracer(coverage.FileTracer):
+class MyFileTracer(FileTracer):
"""A FileTracer emulating a simple static plugin."""
- def __init__(self, filename):
+ def __init__(self, filename: str) -> None:
"""Claim that */*xyz.py was actually sourced from /src/*ABC.zz"""
self._filename = filename
self._source_filename = os.path.join(
@@ -32,21 +38,24 @@
os.path.basename(filename.replace("xyz.py", "ABC.zz"))
)
- def source_filename(self):
+ def source_filename(self) -> str:
return self._source_filename
- def line_number_range(self, frame):
+ def line_number_range(self, frame: FrameType) -> Tuple[TLineNo, TLineNo]:
"""Map the line number X to X05,X06,X07."""
lineno = frame.f_lineno
return lineno*100+5, lineno*100+7
-class FileReporter(coverage.FileReporter):
+class MyFileReporter(FileReporter):
"""Dead-simple FileReporter."""
- def lines(self):
+ def lines(self) -> Set[TLineNo]:
return {105, 106, 107, 205, 206, 207}
-def coverage_init(reg, options): # pylint: disable=unused-argument
+def coverage_init(
+ reg: Plugins,
+ options: Any, # pylint: disable=unused-argument
+) -> None:
"""Called by coverage to initialize the plugins here."""
reg.add_file_tracer(Plugin())
diff -Nru python-coverage-6.5.0+dfsg1/tests/plugin2.py python-coverage-7.2.7+dfsg1/tests/plugin2.py
--- python-coverage-6.5.0+dfsg1/tests/plugin2.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/plugin2.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,9 +3,16 @@
"""A file tracer plugin for test_plugins.py to import."""
+from __future__ import annotations
+
import os.path
-import coverage
+from types import FrameType
+from typing import Any, Optional, Set, Tuple
+
+from coverage import CoveragePlugin, FileReporter, FileTracer
+from coverage.plugin_support import Plugins
+from coverage.types import TLineNo
try:
import third.render # pylint: disable=unused-import
@@ -16,43 +23,50 @@
pass
-class Plugin(coverage.CoveragePlugin):
+class Plugin(CoveragePlugin):
"""A file tracer plugin for testing."""
- def file_tracer(self, filename):
+ def file_tracer(self, filename: str) -> Optional[FileTracer]:
if "render.py" in filename:
return RenderFileTracer()
return None
- def file_reporter(self, filename):
- return FileReporter(filename)
+ def file_reporter(self, filename: str) -> FileReporter:
+ return MyFileReporter(filename)
-class RenderFileTracer(coverage.FileTracer):
+class RenderFileTracer(FileTracer):
"""A FileTracer using information from the caller."""
- def has_dynamic_source_filename(self):
+ def has_dynamic_source_filename(self) -> bool:
return True
- def dynamic_source_filename(self, filename, frame):
+ def dynamic_source_filename(
+ self,
+ filename: str,
+ frame: FrameType,
+ ) -> Optional[str]:
if frame.f_code.co_name != "render":
return None
- source_filename = os.path.abspath(frame.f_locals['filename'])
+ source_filename: str = os.path.abspath(frame.f_locals['filename'])
return source_filename
- def line_number_range(self, frame):
+ def line_number_range(self, frame: FrameType) -> Tuple[TLineNo, TLineNo]:
lineno = frame.f_locals['linenum']
return lineno, lineno+1
-class FileReporter(coverage.FileReporter):
+class MyFileReporter(FileReporter):
"""A goofy file reporter."""
- def lines(self):
+ def lines(self) -> Set[TLineNo]:
# Goofy test arrangement: claim that the file has as many lines as the
# number in its name.
num = os.path.basename(self.filename).split(".")[0].split("_")[1]
return set(range(1, int(num)+1))
-def coverage_init(reg, options): # pylint: disable=unused-argument
+def coverage_init(
+ reg: Plugins,
+ options: Any, # pylint: disable=unused-argument
+) -> None:
"""Called by coverage to initialize the plugins here."""
reg.add_file_tracer(Plugin())
diff -Nru python-coverage-6.5.0+dfsg1/tests/plugin_config.py python-coverage-7.2.7+dfsg1/tests/plugin_config.py
--- python-coverage-6.5.0+dfsg1/tests/plugin_config.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/plugin_config.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,20 +3,29 @@
"""A configuring plugin for test_plugins.py to import."""
+from __future__ import annotations
+
+from typing import Any, List, cast
+
import coverage
+from coverage.plugin_support import Plugins
+from coverage.types import TConfigurable
class Plugin(coverage.CoveragePlugin):
"""A configuring plugin for testing."""
- def configure(self, config):
+ def configure(self, config: TConfigurable) -> None:
"""Configure all the things!"""
opt_name = "report:exclude_lines"
- exclude_lines = config.get_option(opt_name)
+ exclude_lines = cast(List[str], config.get_option(opt_name))
exclude_lines.append(r"pragma: custom")
exclude_lines.append(r"pragma: or whatever")
config.set_option(opt_name, exclude_lines)
-def coverage_init(reg, options): # pylint: disable=unused-argument
+def coverage_init(
+ reg: Plugins,
+ options: Any, # pylint: disable=unused-argument
+) -> None:
"""Called by coverage to initialize the plugins here."""
reg.add_configurer(Plugin())
diff -Nru python-coverage-6.5.0+dfsg1/tests/stress_phystoken_dos.tok python-coverage-7.2.7+dfsg1/tests/stress_phystoken_dos.tok
--- python-coverage-6.5.0+dfsg1/tests/stress_phystoken_dos.tok 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/stress_phystoken_dos.tok 2023-05-29 19:46:30.000000000 +0000
@@ -3,7 +3,7 @@
# Here's some random Python so that test_tokenize_myself will have some
# stressful stuff to try. This file is .tok instead of .py so pylint won't
-# complain about it, check_eol won't look at it, etc.
+# complain about it, editors won't mess with it, etc.
first_back = """\
hey there!
diff -Nru python-coverage-6.5.0+dfsg1/tests/stress_phystoken.tok python-coverage-7.2.7+dfsg1/tests/stress_phystoken.tok
--- python-coverage-6.5.0+dfsg1/tests/stress_phystoken.tok 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/stress_phystoken.tok 2023-05-29 19:46:30.000000000 +0000
@@ -3,7 +3,7 @@
# Here's some random Python so that test_tokenize_myself will have some
# stressful stuff to try. This file is .tok instead of .py so pylint won't
-# complain about it, check_eol won't look at it, etc.
+# complain about it, editors won't mess with it, etc.
# Some lines are here to reproduce fixed bugs in ast_dump also.
first_back = """\
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_annotate.py python-coverage-7.2.7+dfsg1/tests/test_annotate.py
--- python-coverage-6.5.0+dfsg1/tests/test_annotate.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_annotate.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,8 @@
"""Tests for annotation from coverage.py."""
+from __future__ import annotations
+
import coverage
from tests.coveragetest import CoverageTest
@@ -12,7 +14,7 @@
class AnnotationGoldTest(CoverageTest):
"""Test the annotate feature with gold files."""
- def make_multi(self):
+ def make_multi(self) -> None:
"""Make a few source files we need for the tests."""
self.make_file("multi.py", """\
import a.a
@@ -36,7 +38,7 @@
print(msg)
""")
- def test_multi(self):
+ def test_multi(self) -> None:
self.make_multi()
cov = coverage.Coverage()
self.start_import_stop(cov, "multi")
@@ -44,7 +46,7 @@
compare(gold_path("annotate/multi"), ".", "*,cover")
- def test_annotate_dir(self):
+ def test_annotate_dir(self) -> None:
self.make_multi()
cov = coverage.Coverage(source=["."])
self.start_import_stop(cov, "multi")
@@ -52,7 +54,7 @@
compare(gold_path("annotate/anno_dir"), "out_anno_dir", "*,cover")
- def test_encoding(self):
+ def test_encoding(self) -> None:
self.make_file("utf8.py", """\
# -*- coding: utf-8 -*-
# This comment has an accent: é
@@ -64,7 +66,7 @@
cov.annotate()
compare(gold_path("annotate/encodings"), ".", "*,cover")
- def test_white(self):
+ def test_white(self) -> None:
self.make_file("white.py", """\
# A test case sent to me by Steve White
@@ -106,7 +108,7 @@
cov.annotate()
compare(gold_path("annotate/white"), ".", "*,cover")
- def test_missing_after_else(self):
+ def test_missing_after_else(self) -> None:
self.make_file("mae.py", """\
def f(x):
if x == 1:
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_api.py python-coverage-7.2.7+dfsg1/tests/test_api.py
--- python-coverage-6.5.0+dfsg1/tests/test_api.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_api.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,8 @@
"""Tests for coverage.py's API."""
+from __future__ import annotations
+
import fnmatch
import glob
import io
@@ -13,14 +15,17 @@
import sys
import textwrap
+from typing import cast, Callable, Dict, Iterable, List, Optional, Set
+
import pytest
import coverage
-from coverage import env
-from coverage.data import line_counts
+from coverage import Coverage, env
+from coverage.data import line_counts, sorted_lines
from coverage.exceptions import CoverageException, DataError, NoDataError, NoSource
from coverage.files import abs_file, relative_filename
from coverage.misc import import_local_file
+from coverage.types import FilePathClasses, FilePathType, Protocol, TCovKwargs
from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin
from tests.helpers import assert_count_equal, assert_coverage_warnings
@@ -31,7 +36,7 @@
class ApiTest(CoverageTest):
"""Api-oriented tests for coverage.py."""
- def clean_files(self, files, pats):
+ def clean_files(self, files: List[str], pats: List[str]) -> List[str]:
"""Remove names matching `pats` from `files`, a list of file names."""
good = []
for f in files:
@@ -42,13 +47,13 @@
good.append(f)
return good
- def assertFiles(self, files):
+ def assertFiles(self, files: List[str]) -> None:
"""Assert that the files here are `files`, ignoring the usual junk."""
here = os.listdir(".")
here = self.clean_files(here, ["*.pyc", "__pycache__", "*$py.class"])
assert_count_equal(here, files)
- def test_unexecuted_file(self):
+ def test_unexecuted_file(self) -> None:
cov = coverage.Coverage()
self.make_file("mycode.py", """\
@@ -70,8 +75,7 @@
assert statements == [1]
assert missing == [1]
- def test_filenames(self):
-
+ def test_filenames(self) -> None:
self.make_file("mymain.py", """\
import mymod
a = 1
@@ -110,7 +114,7 @@
filename, _, _, _ = cov.analysis(sys.modules["mymod"])
assert os.path.basename(filename) == "mymod.py"
- def test_ignore_stdlib(self):
+ def test_ignore_stdlib(self) -> None:
self.make_file("mymain.py", """\
import colorsys
a = 1
@@ -140,7 +144,7 @@
_, statements, missing, _ = cov2.analysis("colorsys.py")
assert statements != missing
- def test_include_can_measure_stdlib(self):
+ def test_include_can_measure_stdlib(self) -> None:
self.make_file("mymain.py", """\
import colorsys, random
a = 1
@@ -159,7 +163,7 @@
_, statements, missing, _ = cov1.analysis("random.py")
assert statements == missing
- def test_exclude_list(self):
+ def test_exclude_list(self) -> None:
cov = coverage.Coverage()
cov.clear_exclude()
assert cov.get_exclude_list() == []
@@ -171,7 +175,7 @@
cov.clear_exclude()
assert cov.get_exclude_list() == []
- def test_exclude_partial_list(self):
+ def test_exclude_partial_list(self) -> None:
cov = coverage.Coverage()
cov.clear_exclude(which='partial')
assert cov.get_exclude_list(which='partial') == []
@@ -183,7 +187,7 @@
cov.clear_exclude(which='partial')
assert cov.get_exclude_list(which='partial') == []
- def test_exclude_and_partial_are_separate_lists(self):
+ def test_exclude_and_partial_are_separate_lists(self) -> None:
cov = coverage.Coverage()
cov.clear_exclude(which='partial')
cov.clear_exclude(which='exclude')
@@ -204,7 +208,7 @@
assert cov.get_exclude_list(which='partial') == []
assert cov.get_exclude_list(which='exclude') == []
- def test_datafile_default(self):
+ def test_datafile_default(self) -> None:
# Default data file behavior: it's .coverage
self.make_file("datatest1.py", """\
fooey = 17
@@ -216,31 +220,33 @@
cov.save()
self.assertFiles(["datatest1.py", ".coverage"])
- def test_datafile_specified(self):
+ @pytest.mark.parametrize("file_class", FilePathClasses)
+ def test_datafile_specified(self, file_class: FilePathType) -> None:
# You can specify the data file name.
self.make_file("datatest2.py", """\
fooey = 17
""")
self.assertFiles(["datatest2.py"])
- cov = coverage.Coverage(data_file="cov.data")
+ cov = coverage.Coverage(data_file=file_class("cov.data"))
self.start_import_stop(cov, "datatest2")
cov.save()
self.assertFiles(["datatest2.py", "cov.data"])
- def test_datafile_and_suffix_specified(self):
+ @pytest.mark.parametrize("file_class", FilePathClasses)
+ def test_datafile_and_suffix_specified(self, file_class: FilePathType) -> None:
# You can specify the data file name and suffix.
self.make_file("datatest3.py", """\
fooey = 17
""")
self.assertFiles(["datatest3.py"])
- cov = coverage.Coverage(data_file="cov.data", data_suffix="14")
+ cov = coverage.Coverage(data_file=file_class("cov.data"), data_suffix="14")
self.start_import_stop(cov, "datatest3")
cov.save()
self.assertFiles(["datatest3.py", "cov.data.14"])
- def test_datafile_from_rcfile(self):
+ def test_datafile_from_rcfile(self) -> None:
# You can specify the data file name in the .coveragerc file
self.make_file("datatest4.py", """\
fooey = 17
@@ -256,7 +262,7 @@
cov.save()
self.assertFiles(["datatest4.py", ".coveragerc", "mydata.dat"])
- def test_deep_datafile(self):
+ def test_deep_datafile(self) -> None:
self.make_file("datatest5.py", "fooey = 17")
self.assertFiles(["datatest5.py"])
cov = coverage.Coverage(data_file="deep/sub/cov.data")
@@ -265,16 +271,16 @@
self.assertFiles(["datatest5.py", "deep"])
self.assert_exists("deep/sub/cov.data")
- def test_datafile_none(self):
+ def test_datafile_none(self) -> None:
cov = coverage.Coverage(data_file=None)
- def f1(): # pragma: nested
- a = 1 # pylint: disable=unused-variable
+ def f1() -> None: # pragma: nested
+ a = 1 # pylint: disable=unused-variable
one_line_number = f1.__code__.co_firstlineno + 1
lines = []
- def run_one_function(f):
+ def run_one_function(f: Callable[[], None]) -> None:
cov.erase()
cov.start()
f()
@@ -290,14 +296,14 @@
self.assert_doesnt_exist(".coverage")
assert os.listdir(".") == []
- def test_empty_reporting(self):
+ def test_empty_reporting(self) -> None:
# empty summary reports raise exception, just like the xml report
cov = coverage.Coverage()
cov.erase()
with pytest.raises(NoDataError, match="No data to report."):
cov.report()
- def test_completely_zero_reporting(self):
+ def test_completely_zero_reporting(self) -> None:
# https://github.com/nedbat/coveragepy/issues/884
# If nothing was measured, the file-touching didn't happen properly.
self.make_file("foo/bar.py", "print('Never run')")
@@ -316,7 +322,7 @@
last = self.last_line_squeezed(self.stdout())
assert "TOTAL 1 1 0%" == last
- def test_cov4_data_file(self):
+ def test_cov4_data_file(self) -> None:
cov4_data = (
"!coverage.py: This is a private format, don't read it directly!" +
'{"lines":{"/private/tmp/foo.py":[1,5,2,3]}}'
@@ -327,7 +333,7 @@
cov.load()
cov.erase()
- def make_code1_code2(self):
+ def make_code1_code2(self) -> None:
"""Create the code1.py and code2.py files."""
self.make_file("code1.py", """\
code1 = 1
@@ -337,7 +343,7 @@
code2 = 2
""")
- def check_code1_code2(self, cov):
+ def check_code1_code2(self, cov: Coverage) -> None:
"""Check the analysis is correct for code1.py and code2.py."""
_, statements, missing, _ = cov.analysis("code1.py")
assert statements == [1]
@@ -346,7 +352,7 @@
assert statements == [1, 2]
assert missing == []
- def test_start_stop_start_stop(self):
+ def test_start_stop_start_stop(self) -> None:
self.make_code1_code2()
cov = coverage.Coverage()
self.start_import_stop(cov, "code1")
@@ -354,7 +360,7 @@
self.start_import_stop(cov, "code2")
self.check_code1_code2(cov)
- def test_start_save_stop(self):
+ def test_start_save_stop(self) -> None:
self.make_code1_code2()
cov = coverage.Coverage()
cov.start()
@@ -364,7 +370,7 @@
cov.stop() # pragma: nested
self.check_code1_code2(cov)
- def test_start_save_nostop(self):
+ def test_start_save_nostop(self) -> None:
self.make_code1_code2()
cov = coverage.Coverage()
cov.start()
@@ -375,7 +381,7 @@
# Then stop it, or the test suite gets out of whack.
cov.stop() # pragma: nested
- def test_two_getdata_only_warn_once(self):
+ def test_two_getdata_only_warn_once(self) -> None:
self.make_code1_code2()
cov = coverage.Coverage(source=["."], omit=["code1.py"])
cov.start()
@@ -389,7 +395,7 @@
with self.assert_warnings(cov, []):
cov.get_data()
- def test_two_getdata_warn_twice(self):
+ def test_two_getdata_warn_twice(self) -> None:
self.make_code1_code2()
cov = coverage.Coverage(source=["."], omit=["code1.py", "code2.py"])
cov.start()
@@ -404,7 +410,7 @@
# Then stop it, or the test suite gets out of whack.
cov.stop() # pragma: nested
- def make_good_data_files(self):
+ def make_good_data_files(self) -> None:
"""Make some good data files."""
self.make_code1_code2()
cov = coverage.Coverage(data_suffix=True)
@@ -416,7 +422,7 @@
cov.save()
self.assert_file_count(".coverage.*", 2)
- def test_combining_corrupt_data(self):
+ def test_combining_corrupt_data(self) -> None:
# If you combine a corrupt data file, then you will get a warning,
# and the file will remain.
self.make_good_data_files()
@@ -435,7 +441,7 @@
self.assert_exists(".coverage.foo")
self.assert_file_count(".coverage.*", 1)
- def test_combining_twice(self):
+ def test_combining_twice(self) -> None:
self.make_good_data_files()
cov1 = coverage.Coverage()
cov1.combine()
@@ -460,7 +466,7 @@
assert statements == [1, 2]
assert missing == [1, 2]
- def test_combining_with_a_used_coverage(self):
+ def test_combining_with_a_used_coverage(self) -> None:
# Can you use a coverage object to run one shard of a parallel suite,
# and then also combine the data?
self.make_code1_code2()
@@ -476,10 +482,13 @@
assert self.stdout() == ""
self.check_code1_code2(cov)
- def test_ordered_combine(self):
+ def test_ordered_combine(self) -> None:
# https://github.com/nedbat/coveragepy/issues/649
- # The order of the [paths] setting matters
- def make_data_file():
+ # The order of the [paths] setting used to matter. Now the
+ # resulting path must exist, so the order doesn't matter.
+ def make_files() -> None:
+ self.make_file("plugins/p1.py", "")
+ self.make_file("girder/g1.py", "")
self.make_data_file(
basename=".coverage.1",
lines={
@@ -488,7 +497,7 @@
},
)
- def get_combined_filenames():
+ def get_combined_filenames() -> Set[str]:
cov = coverage.Coverage()
cov.combine()
assert self.stdout() == ""
@@ -498,7 +507,7 @@
return filenames
# Case 1: get the order right.
- make_data_file()
+ make_files()
self.make_file(".coveragerc", """\
[paths]
plugins =
@@ -510,8 +519,8 @@
""")
assert get_combined_filenames() == {'girder/g1.py', 'plugins/p1.py'}
- # Case 2: get the order wrong.
- make_data_file()
+ # Case 2: get the order "wrong".
+ make_files()
self.make_file(".coveragerc", """\
[paths]
girder =
@@ -521,9 +530,9 @@
plugins/
ci/girder/plugins/
""")
- assert get_combined_filenames() == {'girder/g1.py', 'girder/plugins/p1.py'}
+ assert get_combined_filenames() == {'girder/g1.py', 'plugins/p1.py'}
- def test_warnings(self):
+ def test_warnings(self) -> None:
self.make_file("hello.py", """\
import sys, os
print("Hello")
@@ -542,7 +551,7 @@
"No data was collected. (no-data-collected)",
)
- def test_warnings_suppressed(self):
+ def test_warnings_suppressed(self) -> None:
self.make_file("hello.py", """\
import sys, os
print("Hello")
@@ -561,7 +570,7 @@
# No "module-not-imported" in warns
# No "no-data-collected" in warns
- def test_warn_once(self):
+ def test_warn_once(self) -> None:
with pytest.warns(Warning) as warns:
cov = coverage.Coverage()
cov.load()
@@ -571,7 +580,7 @@
assert_coverage_warnings(warns, "Warning, warning 1! (bot)")
# No "Warning, warning 2!" in warns
- def test_source_and_include_dont_conflict(self):
+ def test_source_and_include_dont_conflict(self) -> None:
# A bad fix made this case fail: https://github.com/nedbat/coveragepy/issues/541
self.make_file("a.py", "import b\na = 1")
self.make_file("b.py", "b = 1")
@@ -600,7 +609,7 @@
""")
assert expected == self.stdout()
- def make_test_files(self):
+ def make_test_files(self) -> None:
"""Create a simple file representing a method with two tests.
Returns absolute path to the file.
@@ -616,7 +625,7 @@
assert timestwo(6) == 12
""")
- def test_switch_context_testrunner(self):
+ def test_switch_context_testrunner(self) -> None:
# This test simulates a coverage-aware test runner,
# measuring labeled coverage via public API
self.make_test_files()
@@ -649,11 +658,11 @@
suite_filename = filenames['testsuite.py']
data.set_query_context("multiply_six")
- assert [2, 8] == sorted(data.lines(suite_filename))
+ assert [2, 8] == sorted_lines(data, suite_filename)
data.set_query_context("multiply_zero")
- assert [2, 5] == sorted(data.lines(suite_filename))
+ assert [2, 5] == sorted_lines(data, suite_filename)
- def test_switch_context_with_static(self):
+ def test_switch_context_with_static(self) -> None:
# This test simulates a coverage-aware test runner,
# measuring labeled coverage via public API,
# with static label prefix.
@@ -688,11 +697,11 @@
suite_filename = filenames['testsuite.py']
data.set_query_context("mysuite|multiply_six")
- assert [2, 8] == sorted(data.lines(suite_filename))
+ assert [2, 8] == sorted_lines(data, suite_filename)
data.set_query_context("mysuite|multiply_zero")
- assert [2, 5] == sorted(data.lines(suite_filename))
+ assert [2, 5] == sorted_lines(data, suite_filename)
- def test_dynamic_context_conflict(self):
+ def test_dynamic_context_conflict(self) -> None:
cov = coverage.Coverage(source=["."])
cov.set_option("run:dynamic_context", "test_function")
cov.start()
@@ -703,7 +712,13 @@
cov.stop() # pragma: nested
assert_coverage_warnings(warns, "Conflicting dynamic contexts (dynamic-conflict)")
- def test_switch_context_unstarted(self):
+ def test_unknown_dynamic_context(self) -> None:
+ cov = coverage.Coverage()
+ cov.set_option("run:dynamic_context", "no-idea")
+ with pytest.raises(Exception, match="Don't understand dynamic_context setting: 'no-idea'"):
+ cov.start()
+
+ def test_switch_context_unstarted(self) -> None:
# Coverage must be started to switch context
msg = "Cannot switch context, coverage is not started"
cov = coverage.Coverage()
@@ -717,7 +732,7 @@
with pytest.raises(CoverageException, match=msg):
cov.switch_context("test3")
- def test_config_crash(self):
+ def test_config_crash(self) -> None:
# The internal '[run] _crash' setting can be used to artificially raise
# exceptions from inside Coverage.
cov = coverage.Coverage()
@@ -725,20 +740,20 @@
with pytest.raises(Exception, match="Crashing because called by test_config_crash"):
cov.start()
- def test_config_crash_no_crash(self):
+ def test_config_crash_no_crash(self) -> None:
# '[run] _crash' really checks the call stack.
cov = coverage.Coverage()
cov.set_option("run:_crash", "not_my_caller")
cov.start()
cov.stop()
- def test_run_debug_sys(self):
+ def test_run_debug_sys(self) -> None:
# https://github.com/nedbat/coveragepy/issues/907
cov = coverage.Coverage()
cov.start()
d = dict(cov.sys_info()) # pragma: nested
cov.stop() # pragma: nested
- assert d['data_file'].endswith(".coverage")
+ assert cast(str, d['data_file']).endswith(".coverage")
class CurrentInstanceTest(CoverageTest):
@@ -746,7 +761,7 @@
run_in_temp_dir = False
- def assert_current_is_none(self, current):
+ def assert_current_is_none(self, current: Optional[Coverage]) -> None:
"""Assert that a current we expect to be None is correct."""
# During meta-coverage, the None answers will be wrong because the
# overall coverage measurement will still be on the current-stack.
@@ -755,7 +770,7 @@
if not env.METACOV:
assert current is None
- def test_current(self):
+ def test_current(self) -> None:
cur0 = coverage.Coverage.current()
self.assert_current_is_none(cur0)
# Making an instance doesn't make it current.
@@ -779,7 +794,7 @@
class NamespaceModuleTest(UsingModulesMixin, CoverageTest):
"""Test PEP-420 namespace modules."""
- def test_explicit_namespace_module(self):
+ def test_explicit_namespace_module(self) -> None:
self.make_file("main.py", "import namespace_420\n")
cov = coverage.Coverage()
@@ -788,7 +803,7 @@
with pytest.raises(CoverageException, match=r"Module .* has no file"):
cov.analysis(sys.modules['namespace_420'])
- def test_bug_572(self):
+ def test_bug_572(self) -> None:
self.make_file("main.py", "import namespace_420\n")
# Use source=namespace_420 to trigger the check that used to fail,
@@ -799,57 +814,67 @@
cov.report()
-class IncludeOmitTestsMixin(UsingModulesMixin, CoverageTest):
+class CoverageUsePkgs(Protocol):
+ """A number of test classes have the same helper method."""
+ def coverage_usepkgs(
+ self, # pylint: disable=unused-argument
+ **kwargs: TCovKwargs,
+ ) -> Iterable[str]:
+ """Run coverage on usepkgs, return a line summary. kwargs are for Coverage(**kwargs)."""
+ return ""
+
+
+class IncludeOmitTestsMixin(CoverageUsePkgs, UsingModulesMixin, CoverageTest):
"""Test methods for coverage methods taking include and omit."""
- def filenames_in(self, summary, filenames):
- """Assert the `filenames` are in the keys of `summary`."""
+ def filenames_in(self, summary: Iterable[str], filenames: str) -> None:
+ """Assert the `filenames` are in the `summary`."""
for filename in filenames.split():
assert filename in summary
- def filenames_not_in(self, summary, filenames):
- """Assert the `filenames` are not in the keys of `summary`."""
+ def filenames_not_in(self, summary: Iterable[str], filenames: str) -> None:
+ """Assert the `filenames` are not in the `summary`."""
for filename in filenames.split():
assert filename not in summary
- def test_nothing_specified(self):
+ def test_nothing_specified(self) -> None:
result = self.coverage_usepkgs()
self.filenames_in(result, "p1a p1b p2a p2b othera otherb osa osb")
self.filenames_not_in(result, "p1c")
# Because there was no source= specified, we don't search for
- # unexecuted files.
+ # un-executed files.
- def test_include(self):
+ def test_include(self) -> None:
result = self.coverage_usepkgs(include=["*/p1a.py"])
self.filenames_in(result, "p1a")
self.filenames_not_in(result, "p1b p1c p2a p2b othera otherb osa osb")
- def test_include_2(self):
+ def test_include_2(self) -> None:
result = self.coverage_usepkgs(include=["*a.py"])
self.filenames_in(result, "p1a p2a othera osa")
self.filenames_not_in(result, "p1b p1c p2b otherb osb")
- def test_include_as_string(self):
+ def test_include_as_string(self) -> None:
result = self.coverage_usepkgs(include="*a.py")
self.filenames_in(result, "p1a p2a othera osa")
self.filenames_not_in(result, "p1b p1c p2b otherb osb")
- def test_omit(self):
+ def test_omit(self) -> None:
result = self.coverage_usepkgs(omit=["*/p1a.py"])
self.filenames_in(result, "p1b p2a p2b")
self.filenames_not_in(result, "p1a p1c")
- def test_omit_2(self):
+ def test_omit_2(self) -> None:
result = self.coverage_usepkgs(omit=["*a.py"])
self.filenames_in(result, "p1b p2b otherb osb")
self.filenames_not_in(result, "p1a p1c p2a othera osa")
- def test_omit_as_string(self):
+ def test_omit_as_string(self) -> None:
result = self.coverage_usepkgs(omit="*a.py")
self.filenames_in(result, "p1b p2b otherb osb")
self.filenames_not_in(result, "p1a p1c p2a othera osa")
- def test_omit_and_include(self):
+ def test_omit_and_include(self) -> None:
result = self.coverage_usepkgs(include=["*/p1*"], omit=["*/p1a.py"])
self.filenames_in(result, "p1b")
self.filenames_not_in(result, "p1a p1c p2a p2b")
@@ -858,7 +883,7 @@
class SourceIncludeOmitTest(IncludeOmitTestsMixin, CoverageTest):
"""Test using `source`, `include`, and `omit` when measuring code."""
- def setUp(self):
+ def setUp(self) -> None:
super().setUp()
# These tests use the TESTS_DIR/modules files, but they cd into it. To
@@ -873,8 +898,8 @@
)
sys.path.insert(0, abs_file("tests_dir_modules"))
- def coverage_usepkgs(self, **kwargs):
- """Run coverage on usepkgs and return the line summary.
+ def coverage_usepkgs_counts(self, **kwargs: TCovKwargs) -> Dict[str, int]:
+ """Run coverage on usepkgs and return a line summary.
Arguments are passed to the `coverage.Coverage` constructor.
@@ -891,82 +916,86 @@
summary[k[:-3]] = v
return summary
- def test_source_include_exclusive(self):
+ def coverage_usepkgs(self, **kwargs: TCovKwargs) -> Iterable[str]:
+ summary = self.coverage_usepkgs_counts(**kwargs)
+ return list(summary)
+
+ def test_source_include_exclusive(self) -> None:
cov = coverage.Coverage(source=["pkg1"], include=["pkg2"])
with self.assert_warnings(cov, ["--include is ignored because --source is set"]):
cov.start()
cov.stop() # pragma: nested
- def test_source_package_as_package(self):
+ def test_source_package_as_package(self) -> None:
assert not os.path.isdir("pkg1")
- lines = self.coverage_usepkgs(source=["pkg1"])
- self.filenames_in(lines, "p1a p1b")
- self.filenames_not_in(lines, "p2a p2b othera otherb osa osb")
- # Because source= was specified, we do search for unexecuted files.
+ lines = self.coverage_usepkgs_counts(source=["pkg1"])
+ self.filenames_in(list(lines), "p1a p1b")
+ self.filenames_not_in(list(lines), "p2a p2b othera otherb osa osb")
+ # Because source= was specified, we do search for un-executed files.
assert lines['p1c'] == 0
- def test_source_package_as_dir(self):
+ def test_source_package_as_dir(self) -> None:
os.chdir("tests_dir_modules")
assert os.path.isdir("pkg1")
- lines = self.coverage_usepkgs(source=["pkg1"])
- self.filenames_in(lines, "p1a p1b")
- self.filenames_not_in(lines, "p2a p2b othera otherb osa osb")
- # Because source= was specified, we do search for unexecuted files.
+ lines = self.coverage_usepkgs_counts(source=["pkg1"])
+ self.filenames_in(list(lines), "p1a p1b")
+ self.filenames_not_in(list(lines), "p2a p2b othera otherb osa osb")
+ # Because source= was specified, we do search for un-executed files.
assert lines['p1c'] == 0
- def test_source_package_dotted_sub(self):
- lines = self.coverage_usepkgs(source=["pkg1.sub"])
- self.filenames_not_in(lines, "p2a p2b othera otherb osa osb")
- # Because source= was specified, we do search for unexecuted files.
+ def test_source_package_dotted_sub(self) -> None:
+ lines = self.coverage_usepkgs_counts(source=["pkg1.sub"])
+ self.filenames_not_in(list(lines), "p2a p2b othera otherb osa osb")
+ # Because source= was specified, we do search for un-executed files.
assert lines['runmod3'] == 0
- def test_source_package_dotted_p1b(self):
- lines = self.coverage_usepkgs(source=["pkg1.p1b"])
- self.filenames_in(lines, "p1b")
- self.filenames_not_in(lines, "p1a p1c p2a p2b othera otherb osa osb")
+ def test_source_package_dotted_p1b(self) -> None:
+ lines = self.coverage_usepkgs_counts(source=["pkg1.p1b"])
+ self.filenames_in(list(lines), "p1b")
+ self.filenames_not_in(list(lines), "p1a p1c p2a p2b othera otherb osa osb")
- def test_source_package_part_omitted(self):
+ def test_source_package_part_omitted(self) -> None:
# https://github.com/nedbat/coveragepy/issues/218
# Used to be if you omitted something executed and inside the source,
# then after it was executed but not recorded, it would be found in
- # the search for unexecuted files, and given a score of 0%.
+ # the search for un-executed files, and given a score of 0%.
# The omit arg is by path, so need to be in the modules directory.
os.chdir("tests_dir_modules")
- lines = self.coverage_usepkgs(source=["pkg1"], omit=["pkg1/p1b.py"])
- self.filenames_in(lines, "p1a")
- self.filenames_not_in(lines, "p1b")
+ lines = self.coverage_usepkgs_counts(source=["pkg1"], omit=["pkg1/p1b.py"])
+ self.filenames_in(list(lines), "p1a")
+ self.filenames_not_in(list(lines), "p1b")
assert lines['p1c'] == 0
- def test_source_package_as_package_part_omitted(self):
+ def test_source_package_as_package_part_omitted(self) -> None:
# https://github.com/nedbat/coveragepy/issues/638
- lines = self.coverage_usepkgs(source=["pkg1"], omit=["*/p1b.py"])
- self.filenames_in(lines, "p1a")
- self.filenames_not_in(lines, "p1b")
+ lines = self.coverage_usepkgs_counts(source=["pkg1"], omit=["*/p1b.py"])
+ self.filenames_in(list(lines), "p1a")
+ self.filenames_not_in(list(lines), "p1b")
assert lines['p1c'] == 0
- def test_ambiguous_source_package_as_dir(self):
+ def test_ambiguous_source_package_as_dir(self) -> None:
# pkg1 is a directory and a pkg, since we cd into tests_dir_modules/ambiguous
os.chdir("tests_dir_modules/ambiguous")
# pkg1 defaults to directory because tests_dir_modules/ambiguous/pkg1 exists
- lines = self.coverage_usepkgs(source=["pkg1"])
- self.filenames_in(lines, "ambiguous")
- self.filenames_not_in(lines, "p1a p1b p1c")
+ lines = self.coverage_usepkgs_counts(source=["pkg1"])
+ self.filenames_in(list(lines), "ambiguous")
+ self.filenames_not_in(list(lines), "p1a p1b p1c")
- def test_ambiguous_source_package_as_package(self):
+ def test_ambiguous_source_package_as_package(self) -> None:
# pkg1 is a directory and a pkg, since we cd into tests_dir_modules/ambiguous
os.chdir("tests_dir_modules/ambiguous")
- lines = self.coverage_usepkgs(source_pkgs=["pkg1"])
- self.filenames_in(lines, "p1a p1b")
- self.filenames_not_in(lines, "p2a p2b othera otherb osa osb ambiguous")
- # Because source= was specified, we do search for unexecuted files.
+ lines = self.coverage_usepkgs_counts(source_pkgs=["pkg1"])
+ self.filenames_in(list(lines), "p1a p1b")
+ self.filenames_not_in(list(lines), "p2a p2b othera otherb osa osb ambiguous")
+ # Because source= was specified, we do search for un-executed files.
assert lines['p1c'] == 0
class ReportIncludeOmitTest(IncludeOmitTestsMixin, CoverageTest):
"""Tests of the report include/omit functionality."""
- def coverage_usepkgs(self, **kwargs):
+ def coverage_usepkgs(self, **kwargs: TCovKwargs) -> Iterable[str]:
"""Try coverage.report()."""
cov = coverage.Coverage()
cov.start()
@@ -985,7 +1014,7 @@
"""
- def coverage_usepkgs(self, **kwargs):
+ def coverage_usepkgs(self, **kwargs: TCovKwargs) -> Iterable[str]:
"""Try coverage.xml_report()."""
cov = coverage.Coverage()
cov.start()
@@ -997,7 +1026,7 @@
class AnalysisTest(CoverageTest):
"""Test the numerical analysis of results."""
- def test_many_missing_branches(self):
+ def test_many_missing_branches(self) -> None:
cov = coverage.Coverage(branch=True)
self.make_file("missing.py", """\
@@ -1034,7 +1063,7 @@
way they do.
"""
- def pretend_to_be_nose_with_cover(self, erase=False, cd=False):
+ def pretend_to_be_nose_with_cover(self, erase: bool = False, cd: bool = False) -> None:
"""This is what the nose --with-cover plugin does."""
self.make_file("no_biggie.py", """\
a = 1
@@ -1065,17 +1094,17 @@
if cd:
os.chdir("..")
- def test_nose_plugin(self):
+ def test_nose_plugin(self) -> None:
self.pretend_to_be_nose_with_cover()
- def test_nose_plugin_with_erase(self):
+ def test_nose_plugin_with_erase(self) -> None:
self.pretend_to_be_nose_with_cover(erase=True)
- def test_nose_plugin_with_cd(self):
+ def test_nose_plugin_with_cd(self) -> None:
# https://github.com/nedbat/coveragepy/issues/916
self.pretend_to_be_nose_with_cover(cd=True)
- def pretend_to_be_pytestcov(self, append):
+ def pretend_to_be_pytestcov(self, append: bool) -> None:
"""Act like pytest-cov."""
self.make_file("prog.py", """\
a = 1
@@ -1110,16 +1139,17 @@
self.assert_file_count(".coverage", 0)
self.assert_file_count(".coverage.*", 1)
- def test_pytestcov_parallel(self):
+ def test_pytestcov_parallel(self) -> None:
self.pretend_to_be_pytestcov(append=False)
- def test_pytestcov_parallel_append(self):
+ def test_pytestcov_parallel_append(self) -> None:
self.pretend_to_be_pytestcov(append=True)
class ImmutableConfigTest(CoverageTest):
"""Check that reporting methods don't permanently change the configuration."""
- def test_config_doesnt_change(self):
+
+ def test_config_doesnt_change(self) -> None:
self.make_file("simple.py", "a = 1")
cov = coverage.Coverage()
self.start_import_stop(cov, "simple")
@@ -1130,7 +1160,8 @@
class RelativePathTest(CoverageTest):
"""Tests of the relative_files setting."""
- def test_moving_stuff(self):
+
+ def test_moving_stuff(self) -> None:
# When using absolute file names, moving the source around results in
# "No source for code" errors while reporting.
self.make_file("foo.py", "a = 1")
@@ -1149,7 +1180,7 @@
with pytest.raises(NoSource, match=expected):
cov.report()
- def test_moving_stuff_with_relative(self):
+ def test_moving_stuff_with_relative(self) -> None:
# When using relative file names, moving the source around is fine.
self.make_file("foo.py", "a = 1")
self.make_file(".coveragerc", """\
@@ -1171,7 +1202,7 @@
res = cov.report()
assert res == 100
- def test_combine_relative(self):
+ def test_combine_relative(self) -> None:
self.make_file("foo.py", """\
import mod
a = 1
@@ -1197,6 +1228,10 @@
cov.save()
shutil.move(glob.glob(".coverage.*")[0], "..")
+ self.make_file("foo.py", "a = 1")
+ self.make_file("bar.py", "a = 1")
+ self.make_file("modsrc/__init__.py", "x = 1")
+
self.make_file(".coveragerc", """\
[run]
relative_files = true
@@ -1209,10 +1244,6 @@
cov.combine()
cov.save()
- self.make_file("foo.py", "a = 1")
- self.make_file("bar.py", "a = 1")
- self.make_file("modsrc/__init__.py", "x = 1")
-
cov = coverage.Coverage()
cov.load()
files = cov.get_data().measured_files()
@@ -1220,7 +1251,7 @@
res = cov.report()
assert res == 100
- def test_combine_no_suffix_multiprocessing(self):
+ def test_combine_no_suffix_multiprocessing(self) -> None:
self.make_file(".coveragerc", """\
[run]
branch = True
@@ -1240,6 +1271,38 @@
self.assert_file_count(".coverage.*", 0)
self.assert_exists(".coverage")
+ def test_files_up_one_level(self) -> None:
+ # https://github.com/nedbat/coveragepy/issues/1280
+ self.make_file("src/mycode.py", """\
+ def foo():
+ return 17
+ """)
+ self.make_file("test/test_it.py", """\
+ from src.mycode import foo
+ assert foo() == 17
+ """)
+ self.make_file("test/.coveragerc", """\
+ [run]
+ parallel = True
+ relative_files = True
+
+ [paths]
+ source =
+ ../src/
+ */src
+ """)
+ os.chdir("test")
+ sys.path.insert(0, "..")
+ cov1 = coverage.Coverage()
+ self.start_import_stop(cov1, "test_it")
+ cov1.save()
+ cov2 = coverage.Coverage()
+ cov2.combine()
+ cov3 = coverage.Coverage()
+ cov3.load()
+ report = self.get_report(cov3)
+ assert self.last_line_squeezed(report) == "TOTAL 4 0 100%"
+
class CombiningTest(CoverageTest):
"""More tests of combining data."""
@@ -1247,7 +1310,7 @@
B_LINES = {"b_or_c.py": [1, 2, 3, 4, 8, 9]}
C_LINES = {"b_or_c.py": [1, 2, 3, 6, 7, 8, 9]}
- def make_b_or_c_py(self):
+ def make_b_or_c_py(self) -> None:
"""Create b_or_c.py, used in a few of these tests."""
# "b_or_c.py b" will run 6 lines.
# "b_or_c.py c" will run 7 lines.
@@ -1264,7 +1327,7 @@
print('done')
""")
- def test_combine_parallel_data(self):
+ def test_combine_parallel_data(self) -> None:
self.make_b_or_c_py()
self.make_data_file(".coverage.b", lines=self.B_LINES)
self.make_data_file(".coverage.c", lines=self.C_LINES)
@@ -1294,7 +1357,7 @@
data.read()
assert line_counts(data)['b_or_c.py'] == 8
- def test_combine_parallel_data_with_a_corrupt_file(self):
+ def test_combine_parallel_data_with_a_corrupt_file(self) -> None:
self.make_b_or_c_py()
self.make_data_file(".coverage.b", lines=self.B_LINES)
self.make_data_file(".coverage.c", lines=self.C_LINES)
@@ -1324,14 +1387,14 @@
data.read()
assert line_counts(data)['b_or_c.py'] == 8
- def test_combine_no_usable_files(self):
+ def test_combine_no_usable_files(self) -> None:
# https://github.com/nedbat/coveragepy/issues/629
self.make_b_or_c_py()
self.make_data_file(".coverage", lines=self.B_LINES)
# Make bogus data files.
self.make_file(".coverage.bad1", "This isn't a coverage data file.")
- self.make_file(".coverage.bad2", "This isn't a coverage data file.")
+ self.make_file(".coverage.bad2", "This isn't a coverage data file either.")
# Combine the parallel coverage data files into .coverage, but nothing is readable.
cov = coverage.Coverage()
@@ -1356,7 +1419,7 @@
data.read()
assert line_counts(data)['b_or_c.py'] == 6
- def test_combine_parallel_data_in_two_steps(self):
+ def test_combine_parallel_data_in_two_steps(self) -> None:
self.make_b_or_c_py()
self.make_data_file(".coverage.b", lines=self.B_LINES)
@@ -1386,7 +1449,7 @@
data.read()
assert line_counts(data)['b_or_c.py'] == 8
- def test_combine_parallel_data_no_append(self):
+ def test_combine_parallel_data_no_append(self) -> None:
self.make_b_or_c_py()
self.make_data_file(".coverage.b", lines=self.B_LINES)
@@ -1413,7 +1476,7 @@
data.read()
assert line_counts(data)['b_or_c.py'] == 7
- def test_combine_parallel_data_keep(self):
+ def test_combine_parallel_data_keep(self) -> None:
self.make_b_or_c_py()
self.make_data_file(".coverage.b", lines=self.B_LINES)
self.make_data_file(".coverage.c", lines=self.C_LINES)
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_arcs.py python-coverage-7.2.7+dfsg1/tests/test_arcs.py
--- python-coverage-6.5.0+dfsg1/tests/test_arcs.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_arcs.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,27 +3,31 @@
"""Tests for coverage.py's arc measurement."""
+from __future__ import annotations
+
import pytest
from tests.coveragetest import CoverageTest
-from tests.helpers import assert_count_equal, xfail_pypy_3749
+from tests.helpers import assert_count_equal, xfail_pypy38
import coverage
from coverage import env
+from coverage.data import sorted_lines
from coverage.files import abs_file
-skip_cpython_92236 = pytest.mark.skipif(
- env.PYVERSION == (3, 11, 0, "beta", 1, 0),
- reason="Avoid a CPython bug: https://github.com/python/cpython/issues/92236",
- # #92236 is fixed in https://github.com/python/cpython/pull/92722
- # and in https://github.com/python/cpython/pull/92772
+# When a try block ends, does the finally block (incorrectly) jump to the
+# last statement, or does it go the line outside the try block that it
+# should?
+xfail_pypy_3882 = pytest.mark.xfail(
+ env.PYPY and env.PYVERSION[:2] == (3, 8) and env.PYPYVERSION >= (7, 3, 11),
+ reason="https://foss.heptapod.net/pypy/pypy/-/issues/3882",
)
class SimpleArcTest(CoverageTest):
"""Tests for coverage.py's arc measurement."""
- def test_simple_sequence(self):
+ def test_simple_sequence(self) -> None:
self.check_coverage("""\
a = 1
b = 2
@@ -46,7 +50,7 @@
arcz="-{0}2 23 35 5-{0}".format(line1)
)
- def test_function_def(self):
+ def test_function_def(self) -> None:
self.check_coverage("""\
def foo():
a = 2
@@ -55,7 +59,7 @@
""",
arcz=".1 .2 14 2. 4.")
- def test_if(self):
+ def test_if(self) -> None:
self.check_coverage("""\
a = 1
if len([]) == 0:
@@ -71,7 +75,7 @@
""",
arcz=".1 12 23 24 34 4.", arcz_missing="23 34")
- def test_if_else(self):
+ def test_if_else(self) -> None:
self.check_coverage("""\
if len([]) == 0:
a = 2
@@ -89,7 +93,7 @@
""",
arcz=".1 12 25 14 45 5.", arcz_missing="12 25")
- def test_compact_if(self):
+ def test_compact_if(self) -> None:
self.check_coverage("""\
a = 1
if len([]) == 0: a = 2
@@ -106,7 +110,7 @@
""",
arcz=".1 14 45 5. .2 2. 23 3.", arcz_missing="23 3.")
- def test_multiline(self):
+ def test_multiline(self) -> None:
self.check_coverage("""\
a = (
2 +
@@ -118,7 +122,7 @@
arcz=".1 15 5.",
)
- def test_if_return(self):
+ def test_if_return(self) -> None:
self.check_coverage("""\
def if_ret(a):
if a:
@@ -131,7 +135,7 @@
arcz=".1 16 67 7. .2 23 24 3. 45 5.",
)
- def test_dont_confuse_exit_and_else(self):
+ def test_dont_confuse_exit_and_else(self) -> None:
self.check_coverage("""\
def foo():
if foo:
@@ -154,14 +158,10 @@
arcz=".1 16 6. .2 23 3. 25 5.", arcz_missing="25 5."
)
- def test_what_is_the_sound_of_no_lines_clapping(self):
- if env.JYTHON:
- # Jython reports no lines for an empty file.
- arcz_missing=".1 1." # pragma: only jython
- elif env.PYBEHAVIOR.empty_is_empty:
+ def test_what_is_the_sound_of_no_lines_clapping(self) -> None:
+ if env.PYBEHAVIOR.empty_is_empty:
arcz_missing=".1 1."
else:
- # Other Pythons report one line.
arcz_missing=""
self.check_coverage("""\
# __init__.py
@@ -170,7 +170,7 @@
arcz_missing=arcz_missing,
)
- def test_bug_1184(self):
+ def test_bug_1184(self) -> None:
self.check_coverage("""\
def foo(x):
if x:
@@ -191,7 +191,7 @@
class WithTest(CoverageTest):
"""Arc-measuring tests involving context managers."""
- def test_with(self):
+ def test_with(self) -> None:
arcz = ".1 .2 23 34 4. 16 6."
if env.PYBEHAVIOR.exit_through_with:
arcz = arcz.replace("4.", "42 2.")
@@ -206,7 +206,7 @@
arcz=arcz,
)
- def test_with_return(self):
+ def test_with_return(self) -> None:
arcz = ".1 .2 23 34 4. 16 6."
if env.PYBEHAVIOR.exit_through_with:
arcz = arcz.replace("4.", "42 2.")
@@ -221,7 +221,7 @@
arcz=arcz,
)
- def test_bug_146(self):
+ def test_bug_146(self) -> None:
# https://github.com/nedbat/coveragepy/issues/146
arcz = ".1 12 23 34 41 15 5."
if env.PYBEHAVIOR.exit_through_with:
@@ -236,7 +236,7 @@
arcz=arcz,
)
- def test_nested_with_return(self):
+ def test_nested_with_return(self) -> None:
arcz = ".1 .2 23 34 45 56 6. 18 8."
if env.PYBEHAVIOR.exit_through_with:
arcz = arcz.replace("6.", "64 42 2.")
@@ -253,7 +253,7 @@
arcz=arcz,
)
- def test_break_through_with(self):
+ def test_break_through_with(self) -> None:
arcz = ".1 12 23 34 45 15 5."
if env.PYBEHAVIOR.exit_through_with:
arcz = arcz.replace("45", "42 25")
@@ -268,7 +268,7 @@
arcz_missing="15",
)
- def test_continue_through_with(self):
+ def test_continue_through_with(self) -> None:
arcz = ".1 12 23 34 41 15 5."
if env.PYBEHAVIOR.exit_through_with:
arcz = arcz.replace("41", "42 21")
@@ -283,7 +283,7 @@
)
# https://github.com/nedbat/coveragepy/issues/1270
- def test_raise_through_with(self):
+ def test_raise_through_with(self) -> None:
if env.PYBEHAVIOR.exit_through_with:
arcz = ".1 12 27 78 8. 9A A. -23 34 45 53 6-2"
arcz_missing = "6-2 8."
@@ -311,7 +311,7 @@
expected = "line 3 didn't jump to the function exit"
assert self.get_missing_arc_description(cov, 3, -2) == expected
- def test_untaken_raise_through_with(self):
+ def test_untaken_raise_through_with(self) -> None:
if env.PYBEHAVIOR.exit_through_with:
arcz = ".1 12 28 89 9. AB B. -23 34 45 56 53 63 37 7-2"
arcz_missing = "56 63 AB B."
@@ -341,7 +341,7 @@
class LoopArcTest(CoverageTest):
"""Arc-measuring tests involving loops."""
- def test_loop(self):
+ def test_loop(self) -> None:
self.check_coverage("""\
for i in range(10):
a = i
@@ -357,7 +357,7 @@
""",
arcz=".1 12 23 32 24 4.", arcz_missing="23 32")
- def test_nested_loop(self):
+ def test_nested_loop(self) -> None:
self.check_coverage("""\
for i in range(3):
for j in range(3):
@@ -367,7 +367,7 @@
arcz=".1 12 23 32 21 14 4.",
)
- def test_break(self):
+ def test_break(self) -> None:
if env.PYBEHAVIOR.omit_after_jump:
arcz = ".1 12 23 35 15 5."
arcz_missing = "15"
@@ -385,7 +385,7 @@
arcz=arcz, arcz_missing=arcz_missing
)
- def test_continue(self):
+ def test_continue(self) -> None:
if env.PYBEHAVIOR.omit_after_jump:
arcz = ".1 12 23 31 15 5."
arcz_missing = ""
@@ -403,7 +403,7 @@
arcz=arcz, arcz_missing=arcz_missing
)
- def test_nested_breaks(self):
+ def test_nested_breaks(self) -> None:
self.check_coverage("""\
for i in range(3):
for j in range(3):
@@ -415,7 +415,7 @@
""",
arcz=".1 12 23 34 45 25 56 51 67 17 7.", arcz_missing="17 25")
- def test_while_1(self):
+ def test_while_1(self) -> None:
# With "while 1", the loop knows it's constant.
if env.PYBEHAVIOR.keep_constant_test:
arcz = ".1 12 23 34 45 36 62 57 7."
@@ -435,7 +435,7 @@
arcz=arcz,
)
- def test_while_true(self):
+ def test_while_true(self) -> None:
# With "while True", 2.x thinks it's computation,
# 3.x thinks it's constant.
if env.PYBEHAVIOR.keep_constant_test:
@@ -456,7 +456,7 @@
arcz=arcz,
)
- def test_zero_coverage_while_loop(self):
+ def test_zero_coverage_while_loop(self) -> None:
# https://github.com/nedbat/coveragepy/issues/502
self.make_file("main.py", "print('done')")
self.make_file("zero.py", """\
@@ -478,7 +478,7 @@
squeezed = self.squeezed_lines(report)
assert expected in squeezed[3]
- def test_bug_496_continue_in_constant_while(self):
+ def test_bug_496_continue_in_constant_while(self) -> None:
# https://github.com/nedbat/coveragepy/issues/496
# A continue in a while-true needs to jump to the right place.
if env.PYBEHAVIOR.keep_constant_test:
@@ -499,7 +499,7 @@
arcz=arcz
)
- def test_for_if_else_for(self):
+ def test_for_if_else_for(self) -> None:
self.check_coverage("""\
def branches_2(l):
if l:
@@ -526,7 +526,7 @@
arcz_missing="26 6."
)
- def test_for_else(self):
+ def test_for_else(self) -> None:
self.check_coverage("""\
def forelse(seq):
for n in seq:
@@ -541,7 +541,7 @@
arcz=".1 .2 23 32 34 47 26 67 7. 18 89 9."
)
- def test_while_else(self):
+ def test_while_else(self) -> None:
self.check_coverage("""\
def whileelse(seq):
while seq:
@@ -557,7 +557,11 @@
arcz=".1 19 9A A. .2 23 34 45 58 42 27 78 8.",
)
- def test_confusing_for_loop_bug_175(self):
+ def test_confusing_for_loop_bug_175(self) -> None:
+ if env.PYBEHAVIOR.comprehensions_are_functions:
+ extra_arcz = " -22 2-2"
+ else:
+ extra_arcz = ""
self.check_coverage("""\
o = [(1,2), (3,4)]
o = [a for a in o]
@@ -565,7 +569,7 @@
x = tup[0]
y = tup[1]
""",
- arcz=".1 -22 2-2 12 23 34 45 53 3.",
+ arcz=".1 12 23 34 45 53 3." + extra_arcz,
)
self.check_coverage("""\
o = [(1,2), (3,4)]
@@ -573,12 +577,12 @@
x = tup[0]
y = tup[1]
""",
- arcz=".1 12 -22 2-2 23 34 42 2.",
+ arcz=".1 12 23 34 42 2." + extra_arcz,
)
# https://bugs.python.org/issue44672
@pytest.mark.xfail(env.PYVERSION < (3, 10), reason="<3.10 traced final pass incorrectly")
- def test_incorrect_loop_exit_bug_1175(self):
+ def test_incorrect_loop_exit_bug_1175(self) -> None:
self.check_coverage("""\
def wrong_loop(x):
if x:
@@ -595,7 +599,7 @@
# https://bugs.python.org/issue44672
@pytest.mark.xfail(env.PYVERSION < (3, 10), reason="<3.10 traced final pass incorrectly")
- def test_incorrect_if_bug_1175(self):
+ def test_incorrect_if_bug_1175(self) -> None:
self.check_coverage("""\
def wrong_loop(x):
if x:
@@ -610,8 +614,7 @@
arcz_missing="26 3. 6.",
)
- @skip_cpython_92236
- def test_generator_expression(self):
+ def test_generator_expression(self) -> None:
# Generator expression:
self.check_coverage("""\
o = ((1,2), (3,4))
@@ -623,8 +626,7 @@
arcz=".1 -22 2-2 12 23 34 45 53 3.",
)
- @skip_cpython_92236
- def test_generator_expression_another_way(self):
+ def test_generator_expression_another_way(self) -> None:
# https://bugs.python.org/issue44450
# Generator expression:
self.check_coverage("""\
@@ -639,7 +641,11 @@
arcz=".1 -22 2-2 12 25 56 67 75 5.",
)
- def test_other_comprehensions(self):
+ def test_other_comprehensions(self) -> None:
+ if env.PYBEHAVIOR.comprehensions_are_functions:
+ extra_arcz = " -22 2-2"
+ else:
+ extra_arcz = ""
# Set comprehension:
self.check_coverage("""\
o = ((1,2), (3,4))
@@ -648,7 +654,7 @@
x = tup[0]
y = tup[1]
""",
- arcz=".1 -22 2-2 12 23 34 45 53 3.",
+ arcz=".1 12 23 34 45 53 3." + extra_arcz,
)
# Dict comprehension:
self.check_coverage("""\
@@ -658,10 +664,14 @@
x = tup[0]
y = tup[1]
""",
- arcz=".1 -22 2-2 12 23 34 45 53 3.",
+ arcz=".1 12 23 34 45 53 3." + extra_arcz,
)
- def test_multiline_dict_comp(self):
+ def test_multiline_dict_comp(self) -> None:
+ if env.PYBEHAVIOR.comprehensions_are_functions:
+ extra_arcz = " 2-2"
+ else:
+ extra_arcz = ""
# Multiline dict comp:
self.check_coverage("""\
# comment
@@ -676,7 +686,7 @@
}
x = 11
""",
- arcz="-22 2B B-2 2-2"
+ arcz="-22 2B B-2" + extra_arcz,
)
# Multi dict comp:
self.check_coverage("""\
@@ -696,14 +706,14 @@
}
x = 15
""",
- arcz="-22 2F F-2 2-2"
+ arcz="-22 2F F-2" + extra_arcz,
)
class ExceptionArcTest(CoverageTest):
"""Arc-measuring tests involving exception handling."""
- def test_try_except(self):
+ def test_try_except(self) -> None:
self.check_coverage("""\
a, b = 1, 1
try:
@@ -714,7 +724,7 @@
""",
arcz=".1 12 23 36 45 56 6.", arcz_missing="45 56")
- def test_raise_followed_by_statement(self):
+ def test_raise_followed_by_statement(self) -> None:
if env.PYBEHAVIOR.omit_after_jump:
arcz = ".1 12 23 34 46 67 78 8."
arcz_missing = ""
@@ -734,7 +744,7 @@
arcz=arcz, arcz_missing=arcz_missing,
)
- def test_hidden_raise(self):
+ def test_hidden_raise(self) -> None:
self.check_coverage("""\
a, b = 1, 1
def oops(x):
@@ -752,7 +762,7 @@
arcz_missing="3-2 78 8B", arcz_unpredicted="79",
)
- def test_except_with_type(self):
+ def test_except_with_type(self) -> None:
self.check_coverage("""\
a, b = 1, 1
def oops(x):
@@ -773,7 +783,8 @@
arcz_unpredicted="8A",
)
- def test_try_finally(self):
+ @xfail_pypy_3882
+ def test_try_finally(self) -> None:
self.check_coverage("""\
a, c = 1, 1
try:
@@ -815,7 +826,8 @@
arcz_missing="",
)
- def test_finally_in_loop(self):
+ @xfail_pypy_3882
+ def test_finally_in_loop(self) -> None:
self.check_coverage("""\
a, c, d, i = 1, 1, 1, 99
try:
@@ -854,7 +866,8 @@
)
- def test_break_through_finally(self):
+ @xfail_pypy_3882
+ def test_break_through_finally(self) -> None:
arcz = ".1 12 23 34 3D 45 56 67 68 7A AD 8A A3 BC CD D."
if env.PYBEHAVIOR.finally_jumps_back:
arcz = arcz.replace("AD", "A7 7D")
@@ -877,7 +890,7 @@
arcz_missing="3D BC CD",
)
- def test_break_continue_without_finally(self):
+ def test_break_continue_without_finally(self) -> None:
self.check_coverage("""\
a, c, d, i = 1, 1, 1, 99
try:
@@ -897,14 +910,15 @@
arcz_missing="3D 9A A3 BC CD",
)
- def test_continue_through_finally(self):
+ @xfail_pypy_3882
+ def test_continue_through_finally(self) -> None:
arcz = ".1 12 23 34 3D 45 56 67 68 7A 8A A3 BC CD D."
if env.PYBEHAVIOR.finally_jumps_back:
arcz += " 73 A7"
self.check_coverage("""\
a, b, c, d, i = 1, 1, 1, 1, 99
try:
- for i in range(5):
+ for i in range(3):
try:
a = 5
if i > 0:
@@ -920,7 +934,7 @@
arcz_missing="BC CD",
)
- def test_finally_in_loop_bug_92(self):
+ def test_finally_in_loop_bug_92(self) -> None:
self.check_coverage("""\
for i in range(5):
try:
@@ -933,7 +947,7 @@
arcz=".1 12 23 35 56 61 17 7.",
)
- def test_bug_212(self):
+ def test_bug_212(self) -> None:
# "except Exception as e" is crucial here.
# Bug 212 said that the "if exc" line was incorrectly marked as only
# partially covered.
@@ -958,7 +972,7 @@
arcz_unpredicted="CD",
)
- def test_except_finally(self):
+ def test_except_finally(self) -> None:
self.check_coverage("""\
a, b, c = 1, 1, 1
try:
@@ -987,7 +1001,7 @@
arcz=".1 12 -23 3-2 24 45 56 67 7B 89 9B BC C.",
arcz_missing="67 7B", arcz_unpredicted="68")
- def test_multiple_except_clauses(self):
+ def test_multiple_except_clauses(self) -> None:
self.check_coverage("""\
a, b, c = 1, 1, 1
try:
@@ -1055,7 +1069,7 @@
arcz_unpredicted="45 7A AB",
)
- def test_return_finally(self):
+ def test_return_finally(self) -> None:
arcz = ".1 12 29 9A AB BC C-1 -23 34 45 7-2 57 38 8-2"
if env.PYBEHAVIOR.finally_jumps_back:
arcz = arcz.replace("7-2", "75 5-2")
@@ -1076,7 +1090,8 @@
arcz=arcz,
)
- def test_except_jump_finally(self):
+ @xfail_pypy_3882
+ def test_except_jump_finally(self) -> None:
arcz = (
".1 1Q QR RS ST TU U. " +
".2 23 34 45 56 4O 6L " +
@@ -1123,7 +1138,8 @@
arcz_unpredicted="67",
)
- def test_else_jump_finally(self):
+ @xfail_pypy_3882
+ def test_else_jump_finally(self) -> None:
arcz = (
".1 1S ST TU UV VW W. " +
".2 23 34 45 56 6A 78 8N 4Q " +
@@ -1176,8 +1192,7 @@
class YieldTest(CoverageTest):
"""Arc tests for generators."""
- @skip_cpython_92236
- def test_yield_in_loop(self):
+ def test_yield_in_loop(self) -> None:
self.check_coverage("""\
def gen(inp):
for n in inp:
@@ -1188,8 +1203,7 @@
arcz=".1 .2 23 2. 32 15 5.",
)
- @skip_cpython_92236
- def test_padded_yield_in_loop(self):
+ def test_padded_yield_in_loop(self) -> None:
self.check_coverage("""\
def gen(inp):
i = 2
@@ -1204,8 +1218,7 @@
arcz=".1 19 9. .2 23 34 45 56 63 37 7.",
)
- @skip_cpython_92236
- def test_bug_308(self):
+ def test_bug_308(self) -> None:
self.check_coverage("""\
def run():
for i in range(10):
@@ -1239,8 +1252,7 @@
arcz=".1 14 45 54 4. .2 2. -22 2-2",
)
- @skip_cpython_92236
- def test_bug_324(self):
+ def test_bug_324(self) -> None:
# This code is tricky: the list() call pulls all the values from gen(),
# but each of them is a generator itself that is never iterated. As a
# result, the generator expression on line 3 is never entered or run.
@@ -1258,8 +1270,7 @@
arcz_missing="-33 3-3",
)
- @skip_cpython_92236
- def test_coroutines(self):
+ def test_coroutines(self) -> None:
self.check_coverage("""\
def double_inputs():
while len([1]): # avoid compiler differences
@@ -1278,8 +1289,7 @@
)
assert self.stdout() == "20\n12\n"
- @skip_cpython_92236
- def test_yield_from(self):
+ def test_yield_from(self) -> None:
self.check_coverage("""\
def gen(inp):
i = 2
@@ -1294,8 +1304,7 @@
arcz=".1 19 9. .2 23 34 45 56 63 37 7.",
)
- @skip_cpython_92236
- def test_abandoned_yield(self):
+ def test_abandoned_yield(self) -> None:
# https://github.com/nedbat/coveragepy/issues/440
self.check_coverage("""\
def gen():
@@ -1315,7 +1324,7 @@
@pytest.mark.skipif(not env.PYBEHAVIOR.match_case, reason="Match-case is new in 3.10")
class MatchCaseTest(CoverageTest):
"""Tests of match-case."""
- def test_match_case_with_default(self):
+ def test_match_case_with_default(self) -> None:
self.check_coverage("""\
for command in ["huh", "go home", "go n"]:
match command.split():
@@ -1331,7 +1340,7 @@
)
assert self.stdout() == "default\nno go\ngo: n\n"
- def test_match_case_with_wildcard(self):
+ def test_match_case_with_wildcard(self) -> None:
self.check_coverage("""\
for command in ["huh", "go home", "go n"]:
match command.split():
@@ -1347,7 +1356,7 @@
)
assert self.stdout() == "default: ['huh']\nno go\ngo: n\n"
- def test_match_case_without_wildcard(self):
+ def test_match_case_without_wildcard(self) -> None:
self.check_coverage("""\
match = None
for command in ["huh", "go home", "go n"]:
@@ -1362,11 +1371,24 @@
)
assert self.stdout() == "None\nno go\ngo: n\n"
+ def test_absurd_wildcard(self) -> None:
+ # https://github.com/nedbat/coveragepy/issues/1421
+ self.check_coverage("""\
+ def absurd(x):
+ match x:
+ case (3 | 99 | (999 | _)):
+ print("default")
+ absurd(5)
+ """,
+ arcz=".1 15 5. .2 23 34 4.",
+ )
+ assert self.stdout() == "default\n"
+
class OptimizedIfTest(CoverageTest):
"""Tests of if statements being optimized away."""
- def test_optimized_away_if_0(self):
+ def test_optimized_away_if_0(self) -> None:
if env.PYBEHAVIOR.keep_constant_test:
lines = [1, 2, 3, 4, 8, 9]
arcz = ".1 12 23 24 34 48 49 89 9."
@@ -1395,7 +1417,7 @@
arcz_missing=arcz_missing,
)
- def test_optimized_away_if_1(self):
+ def test_optimized_away_if_1(self) -> None:
if env.PYBEHAVIOR.keep_constant_test:
lines = [1, 2, 3, 4, 5, 6, 9]
arcz = ".1 12 23 24 34 45 49 56 69 59 9."
@@ -1424,7 +1446,7 @@
arcz_missing=arcz_missing,
)
- def test_optimized_away_if_1_no_else(self):
+ def test_optimized_away_if_1_no_else(self) -> None:
if env.PYBEHAVIOR.keep_constant_test:
lines = [1, 2, 3, 4, 5]
arcz = ".1 12 23 25 34 45 5."
@@ -1448,7 +1470,7 @@
arcz_missing=arcz_missing,
)
- def test_optimized_if_nested(self):
+ def test_optimized_if_nested(self) -> None:
if env.PYBEHAVIOR.keep_constant_test:
lines = [1, 2, 8, 11, 12, 13, 14, 15]
arcz = ".1 12 28 2F 8B 8F BC CD DE EF F."
@@ -1483,7 +1505,7 @@
arcz_missing=arcz_missing,
)
- def test_dunder_debug(self):
+ def test_dunder_debug(self) -> None:
# Since some of our tests use __debug__, let's make sure it is true as
# we expect
assert __debug__
@@ -1499,7 +1521,7 @@
"""
)
- def test_if_debug(self):
+ def test_if_debug(self) -> None:
if env.PYBEHAVIOR.optimize_if_debug:
arcz = ".1 12 24 41 26 61 1."
arcz_missing = ""
@@ -1518,7 +1540,8 @@
arcz_missing=arcz_missing,
)
- def test_if_not_debug(self):
+ @xfail_pypy_3882
+ def test_if_not_debug(self) -> None:
if env.PYBEHAVIOR.optimize_if_not_debug == 1:
arcz = ".1 12 23 34 42 37 72 28 8."
elif env.PYBEHAVIOR.optimize_if_not_debug == 2:
@@ -1544,7 +1567,7 @@
class MiscArcTest(CoverageTest):
"""Miscellaneous arc-measuring tests."""
- def test_dict_literal(self):
+ def test_dict_literal(self) -> None:
self.check_coverage("""\
d = {
'a': 2,
@@ -1572,7 +1595,7 @@
arcz=".1 19 9.",
)
- def test_unpacked_literals(self):
+ def test_unpacked_literals(self) -> None:
self.check_coverage("""\
d = {
'a': 2,
@@ -1603,7 +1626,7 @@
)
@pytest.mark.parametrize("n", [10, 50, 100, 500, 1000, 2000, 10000])
- def test_pathologically_long_code_object(self, n):
+ def test_pathologically_long_code_object(self, n: int) -> None:
# https://github.com/nedbat/coveragepy/issues/359
# Long code objects sometimes cause problems. Originally, it was
# due to EXTENDED_ARG bytes codes. Then it showed a mistake in
@@ -1622,8 +1645,7 @@
self.check_coverage(code, arcs=[(-1, 1), (1, 2*n+4), (2*n+4, -1)])
assert self.stdout() == f"{n}\n"
- @skip_cpython_92236
- def test_partial_generators(self):
+ def test_partial_generators(self) -> None:
# https://github.com/nedbat/coveragepy/issues/475
# Line 2 is executed completely.
# Line 3 is started but not finished, because zip ends before it finishes.
@@ -1649,8 +1671,7 @@
class DecoratorArcTest(CoverageTest):
"""Tests of arcs with decorators."""
- @xfail_pypy_3749
- def test_function_decorator(self):
+ def test_function_decorator(self) -> None:
arcz = (
".1 16 67 7A AE EF F. " # main line
".2 24 4. -23 3-2 " # decorators
@@ -1678,8 +1699,8 @@
arcz=arcz,
)
- @xfail_pypy_3749
- def test_class_decorator(self):
+ @xfail_pypy38
+ def test_class_decorator(self) -> None:
arcz = (
".1 16 67 6D 7A AE E. " # main line
".2 24 4. -23 3-2 " # decorators
@@ -1706,8 +1727,7 @@
arcz=arcz,
)
- @xfail_pypy_3749
- def test_bug_466a(self):
+ def test_bug_466a(self) -> None:
# A bad interaction between decorators and multi-line list assignments,
# believe it or not...!
arcz = ".1 1A A. 13 3. -35 58 8-3 "
@@ -1731,8 +1751,7 @@
arcz=arcz,
)
- @xfail_pypy_3749
- def test_bug_466b(self):
+ def test_bug_466b(self) -> None:
# A bad interaction between decorators and multi-line list assignments,
# believe it or not...!
arcz = ".1 1A A. 13 3. -35 58 8-3 "
@@ -1759,7 +1778,7 @@
class LambdaArcTest(CoverageTest):
"""Tests of lambdas"""
- def test_multiline_lambda(self):
+ def test_multiline_lambda(self) -> None:
self.check_coverage("""\
fn = (lambda x:
x + 2
@@ -1783,7 +1802,7 @@
arcz="-22 2A A-2 2-2",
)
- def test_unused_lambdas_are_confusing_bug_90(self):
+ def test_unused_lambdas_are_confusing_bug_90(self) -> None:
self.check_coverage("""\
a = 1
fn = lambda x: x
@@ -1792,7 +1811,7 @@
arcz=".1 12 -22 2-2 23 3.", arcz_missing="-22 2-2",
)
- def test_raise_with_lambda_looks_like_partial_branch(self):
+ def test_raise_with_lambda_looks_like_partial_branch(self) -> None:
self.check_coverage("""\
def ouch(fn):
2/0
@@ -1813,7 +1832,7 @@
arcz_unpredicted="58",
)
- def test_lambda_in_dict(self):
+ def test_lambda_in_dict(self) -> None:
self.check_coverage("""\
x = 1
x = 2
@@ -1843,8 +1862,7 @@
"""Tests of the new async and await keywords in Python 3.5"""
@xfail_eventlet_670
- @skip_cpython_92236
- def test_async(self):
+ def test_async(self) -> None:
self.check_coverage("""\
import asyncio
@@ -1871,8 +1889,7 @@
assert self.stdout() == "Compute 1 + 2 ...\n1 + 2 = 3\n"
@xfail_eventlet_670
- @skip_cpython_92236
- def test_async_for(self):
+ def test_async_for(self) -> None:
self.check_coverage("""\
import asyncio
@@ -1909,7 +1926,7 @@
)
assert self.stdout() == "a\nb\nc\n.\n"
- def test_async_with(self):
+ def test_async_with(self) -> None:
if env.PYBEHAVIOR.exit_through_with:
arcz = ".1 1. .2 23 32 2."
arcz_missing = ".2 23 32 2."
@@ -1925,8 +1942,7 @@
arcz_missing=arcz_missing,
)
- @xfail_pypy_3749
- def test_async_decorator(self):
+ def test_async_decorator(self) -> None:
arcz = ".1 14 4. .2 2. -46 6-4 "
if env.PYBEHAVIOR.trace_decorated_def:
arcz = arcz.replace("4.", "45 5.")
@@ -1947,8 +1963,7 @@
# https://github.com/nedbat/coveragepy/issues/1158
# https://bugs.python.org/issue44621
@pytest.mark.skipif(env.PYVERSION[:2] == (3, 9), reason="avoid a 3.9 bug: 44621")
- @skip_cpython_92236
- def test_bug_1158(self):
+ def test_bug_1158(self) -> None:
self.check_coverage("""\
import asyncio
@@ -1973,8 +1988,7 @@
# https://github.com/nedbat/coveragepy/issues/1176
# https://bugs.python.org/issue44622
@xfail_eventlet_670
- @skip_cpython_92236
- def test_bug_1176(self):
+ def test_bug_1176(self) -> None:
self.check_coverage("""\
import asyncio
@@ -1992,7 +2006,7 @@
assert self.stdout() == "12\n"
# https://github.com/nedbat/coveragepy/issues/1205
- def test_bug_1205(self):
+ def test_bug_1205(self) -> None:
self.check_coverage("""\
def func():
if T(2):
@@ -2016,7 +2030,7 @@
class AnnotationTest(CoverageTest):
"""Tests using type annotations."""
- def test_annotations(self):
+ def test_annotations(self) -> None:
self.check_coverage("""\
def f(x:str, y:int) -> str:
a:int = 2
@@ -2031,7 +2045,7 @@
class ExcludeTest(CoverageTest):
"""Tests of exclusions to indicate known partial branches."""
- def test_default(self):
+ def test_default(self) -> None:
# A number of forms of pragma comment are accepted.
self.check_coverage("""\
a = 1
@@ -2048,7 +2062,7 @@
arcz=".1 12 23 24 34 45 56 57 67 78 89 9. 8.",
)
- def test_custom_pragmas(self):
+ def test_custom_pragmas(self) -> None:
self.check_coverage("""\
a = 1
while a: # [only some]
@@ -2065,7 +2079,7 @@
class LineDataTest(CoverageTest):
"""Tests that line_data gives us what we expect."""
- def test_branch(self):
+ def test_branch(self) -> None:
cov = coverage.Coverage(branch=True)
self.make_file("fun1.py", """\
@@ -2079,5 +2093,5 @@
self.start_import_stop(cov, "fun1")
data = cov.get_data()
- fun1_lines = data.lines(abs_file("fun1.py"))
+ fun1_lines = sorted_lines(data, abs_file("fun1.py"))
assert_count_equal(fun1_lines, [1, 2, 5])
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_cmdline.py python-coverage-7.2.7+dfsg1/tests/test_cmdline.py
--- python-coverage-6.5.0+dfsg1/tests/test_cmdline.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_cmdline.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,8 @@
"""Test cmdline.py for coverage.py."""
+from __future__ import annotations
+
import ast
import pprint
import re
@@ -10,6 +12,8 @@
import textwrap
from unittest import mock
+from typing import Any, List, Mapping, Optional, Tuple
+
import pytest
import coverage
@@ -18,6 +22,7 @@
from coverage.control import DEFAULT_DATAFILE
from coverage.config import CoverageConfig
from coverage.exceptions import _ExceptionDuringRun
+from coverage.types import TConfigValueIn, TConfigValueOut
from coverage.version import __url__
from tests.coveragetest import CoverageTest, OK, ERR, command_line
@@ -43,8 +48,8 @@
)
_defaults.Coverage().report(
ignore_errors=None, include=None, omit=None, morfs=[],
- show_missing=None, skip_covered=None, contexts=None, skip_empty=None, precision=None,
- sort=None,
+ show_missing=None, skip_covered=None, contexts=None, skip_empty=None,
+ precision=None, sort=None, output_format=None,
)
_defaults.Coverage().xml_report(
ignore_errors=None, include=None, omit=None, morfs=[], outfile=None,
@@ -67,7 +72,7 @@
DEFAULT_KWARGS = {name: kw for name, _, kw in _defaults.mock_calls}
- def model_object(self):
+ def model_object(self) -> mock.Mock:
"""Return a Mock suitable for use in CoverageScript."""
mk = mock.Mock()
@@ -90,7 +95,11 @@
# Global names in cmdline.py that will be mocked during the tests.
MOCK_GLOBALS = ['Coverage', 'PyRunner', 'show_help']
- def mock_command_line(self, args, options=None):
+ def mock_command_line(
+ self,
+ args: str,
+ options: Optional[Mapping[str, TConfigValueIn]] = None,
+ ) -> Tuple[mock.Mock, int]:
"""Run `args` through the command line, with a Mock.
`options` is a dict of names and values to pass to `set_option`.
@@ -118,7 +127,13 @@
return mk, ret
- def cmd_executes(self, args, code, ret=OK, options=None):
+ def cmd_executes(
+ self,
+ args: str,
+ code: str,
+ ret: int = OK,
+ options: Optional[Mapping[str, TConfigValueIn]] = None,
+ ) -> None:
"""Assert that the `args` end up executing the sequence in `code`."""
called, status = self.mock_command_line(args, options=options)
assert status == ret, f"Wrong status: got {status!r}, wanted {ret!r}"
@@ -127,7 +142,7 @@
code = textwrap.dedent(code)
expected = self.model_object()
globs = {n: getattr(expected, n) for n in self.MOCK_GLOBALS}
- code_obj = compile(code, "", "exec")
+ code_obj = compile(code, "", "exec", dont_inherit=True)
eval(code_obj, globs, {}) # pylint: disable=eval-used
# Many of our functions take a lot of arguments, and cmdline.py
@@ -140,14 +155,14 @@
self.assert_same_mock_calls(expected, called)
- def cmd_executes_same(self, args1, args2):
+ def cmd_executes_same(self, args1: str, args2: str) -> None:
"""Assert that the `args1` executes the same as `args2`."""
m1, r1 = self.mock_command_line(args1)
m2, r2 = self.mock_command_line(args2)
assert r1 == r2
self.assert_same_mock_calls(m1, m2)
- def assert_same_mock_calls(self, m1, m2):
+ def assert_same_mock_calls(self, m1: mock.Mock, m2: mock.Mock) -> None:
"""Assert that `m1.mock_calls` and `m2.mock_calls` are the same."""
# Use a real equality comparison, but if it fails, use a nicer assert
# so we can tell what's going on. We have to use the real == first due
@@ -157,7 +172,13 @@
pp2 = pprint.pformat(m2.mock_calls)
assert pp1+'\n' == pp2+'\n'
- def cmd_help(self, args, help_msg=None, topic=None, ret=ERR):
+ def cmd_help(
+ self,
+ args: str,
+ help_msg: Optional[str] = None,
+ topic: Optional[str] = None,
+ ret: int = ERR,
+ ) -> None:
"""Run a command line, and check that it prints the right help.
Only the last function call in the mock is checked, which should be the
@@ -174,7 +195,7 @@
class BaseCmdLineTestTest(BaseCmdLineTest):
"""Tests that our BaseCmdLineTest helpers work."""
- def test_cmd_executes_same(self):
+ def test_cmd_executes_same(self) -> None:
# All the other tests here use self.cmd_executes_same in successful
# ways, so here we just check that it fails.
with pytest.raises(AssertionError):
@@ -184,7 +205,7 @@
class CmdLineTest(BaseCmdLineTest):
"""Tests of the coverage.py command line."""
- def test_annotate(self):
+ def test_annotate(self) -> None:
# coverage annotate [-d DIR] [-i] [--omit DIR,...] [FILE1 FILE2 ...]
self.cmd_executes("annotate", """\
cov = Coverage()
@@ -222,7 +243,7 @@
cov.annotate(morfs=["mod1", "mod2", "mod3"])
""")
- def test_combine(self):
+ def test_combine(self) -> None:
# coverage combine with args
self.cmd_executes("combine datadir1", """\
cov = Coverage()
@@ -259,7 +280,7 @@
cov.save()
""")
- def test_combine_doesnt_confuse_options_with_args(self):
+ def test_combine_doesnt_confuse_options_with_args(self) -> None:
# https://github.com/nedbat/coveragepy/issues/385
self.cmd_executes("combine --rcfile cov.ini", """\
cov = Coverage(config_file='cov.ini')
@@ -277,33 +298,39 @@
("debug foo", "Don't know what you mean by 'foo'"),
("debug sys config", "Only one topic at a time, please"),
])
- def test_debug(self, cmd, output):
+ def test_debug(self, cmd: str, output: str) -> None:
self.cmd_help(cmd, output)
- def test_debug_sys(self):
+ def test_debug_sys(self) -> None:
self.command_line("debug sys")
out = self.stdout()
assert "version:" in out
assert "data_file:" in out
- def test_debug_config(self):
+ def test_debug_config(self) -> None:
self.command_line("debug config")
out = self.stdout()
assert "cover_pylib:" in out
assert "skip_covered:" in out
assert "skip_empty:" in out
- def test_debug_pybehave(self):
+ def test_debug_pybehave(self) -> None:
self.command_line("debug pybehave")
out = self.stdout()
assert " CPYTHON:" in out
assert " PYVERSION:" in out
assert " pep626:" in out
+
+ # Some things that shouldn't appear..
+ assert "typing." not in out # import from typing
+ assert ": <" not in out # objects without a good repr
+
+ # It should report PYVERSION correctly.
pyversion = re_line(r" PYVERSION:", out)
vtuple = ast.literal_eval(pyversion.partition(":")[-1].strip())
assert vtuple[:5] == sys.version_info
- def test_debug_premain(self):
+ def test_debug_premain(self) -> None:
self.command_line("debug premain")
out = self.stdout()
# ... many lines ...
@@ -317,7 +344,7 @@
assert re.search(r"(?m)^\s+command_line : .*[/\\]coverage[/\\]cmdline.py:\d+$", out)
assert re.search(r"(?m)^\s+do_debug : .*[/\\]coverage[/\\]cmdline.py:\d+$", out)
- def test_erase(self):
+ def test_erase(self) -> None:
# coverage erase
self.cmd_executes("erase", """\
cov = Coverage()
@@ -328,23 +355,23 @@
cov.erase()
""")
- def test_version(self):
+ def test_version(self) -> None:
# coverage --version
self.cmd_help("--version", topic="version", ret=OK)
- def test_help_option(self):
+ def test_help_option(self) -> None:
# coverage -h
self.cmd_help("-h", topic="help", ret=OK)
self.cmd_help("--help", topic="help", ret=OK)
- def test_help_command(self):
+ def test_help_command(self) -> None:
self.cmd_executes("help", "show_help(topic='help')")
- def test_cmd_help(self):
+ def test_cmd_help(self) -> None:
self.cmd_executes("run --help", "show_help(parser='')")
self.cmd_executes_same("help run", "run --help")
- def test_html(self):
+ def test_html(self) -> None:
# coverage html -d DIR [-i] [--omit DIR,...] [FILE1 FILE2 ...]
self.cmd_executes("html", """\
cov = Coverage()
@@ -402,7 +429,7 @@
cov.html_report()
""")
- def test_json(self):
+ def test_json(self) -> None:
# coverage json [-i] [--omit DIR,...] [FILE1 FILE2 ...]
self.cmd_executes("json", """\
cov = Coverage()
@@ -465,7 +492,7 @@
cov.json_report()
""")
- def test_lcov(self):
+ def test_lcov(self) -> None:
# coverage lcov [-i] [--omit DIR,...] [FILE1 FILE2 ...]
self.cmd_executes("lcov", """\
cov = Coverage()
@@ -508,7 +535,7 @@
cov.lcov_report()
""")
- def test_report(self):
+ def test_report(self) -> None:
# coverage report [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...]
self.cmd_executes("report", """\
cov = Coverage()
@@ -585,8 +612,13 @@
cov.load()
cov.report(show_missing=None)
""")
+ self.cmd_executes("report --format=markdown", """\
+ cov = Coverage()
+ cov.load()
+ cov.report(output_format="markdown")
+ """)
- def test_run(self):
+ def test_run(self) -> None:
# coverage run [-p] [-L] [--timid] MODULE.py [ARG1 ARG2 ...]
# run calls coverage.erase first.
@@ -721,7 +753,7 @@
cov.save()
""")
- def test_multiprocessing_needs_config_file(self):
+ def test_multiprocessing_needs_config_file(self) -> None:
# You can't use command-line args to add options to multiprocessing
# runs, since they won't make it to the subprocesses. You need to use a
# config file.
@@ -731,7 +763,7 @@
assert msg in err
assert "Remove --branch from the command line." in err
- def test_run_debug(self):
+ def test_run_debug(self) -> None:
self.cmd_executes("run --debug=opt1 foo.py", """\
cov = Coverage(debug=["opt1"])
runner = PyRunner(['foo.py'], as_module=False)
@@ -751,7 +783,7 @@
cov.save()
""")
- def test_run_module(self):
+ def test_run_module(self) -> None:
self.cmd_executes("run -m mymodule", """\
cov = Coverage()
runner = PyRunner(['mymodule'], as_module=True)
@@ -781,11 +813,11 @@
""")
self.cmd_executes_same("run -m mymodule", "run --module mymodule")
- def test_run_nothing(self):
+ def test_run_nothing(self) -> None:
self.command_line("run", ret=ERR)
assert "Nothing to do" in self.stderr()
- def test_run_from_config(self):
+ def test_run_from_config(self) -> None:
options = {"run:command_line": "myprog.py a 123 'a quoted thing' xyz"}
self.cmd_executes("run", """\
cov = Coverage()
@@ -799,7 +831,7 @@
options=options,
)
- def test_run_module_from_config(self):
+ def test_run_module_from_config(self) -> None:
self.cmd_executes("run", """\
cov = Coverage()
runner = PyRunner(['mymodule', 'thing1', 'thing2'], as_module=True)
@@ -812,7 +844,7 @@
options={"run:command_line": "-m mymodule thing1 thing2"},
)
- def test_run_from_config_but_empty(self):
+ def test_run_from_config_but_empty(self) -> None:
self.cmd_executes("run", """\
cov = Coverage()
show_help('Nothing to do.')
@@ -821,7 +853,7 @@
options={"run:command_line": ""},
)
- def test_run_dashm_only(self):
+ def test_run_dashm_only(self) -> None:
self.cmd_executes("run -m", """\
cov = Coverage()
show_help('No module specified for -m')
@@ -836,11 +868,11 @@
options={"run:command_line": "myprog.py"}
)
- def test_cant_append_parallel(self):
+ def test_cant_append_parallel(self) -> None:
self.command_line("run --append --parallel-mode foo.py", ret=ERR)
assert "Can't append to data files in parallel mode." in self.stderr()
- def test_xml(self):
+ def test_xml(self) -> None:
# coverage xml [-i] [--omit DIR,...] [FILE1 FILE2 ...]
self.cmd_executes("xml", """\
cov = Coverage()
@@ -893,10 +925,10 @@
cov.xml_report()
""")
- def test_no_arguments_at_all(self):
+ def test_no_arguments_at_all(self) -> None:
self.cmd_help("", topic="minimum_help", ret=OK)
- def test_bad_command(self):
+ def test_bad_command(self) -> None:
self.cmd_help("xyzzy", "Unknown command: 'xyzzy'")
@@ -905,7 +937,7 @@
run_in_temp_dir = True
- def test_debug_data(self):
+ def test_debug_data(self) -> None:
data = self.make_data_file(
lines={
"file1.py": range(1, 18),
@@ -924,7 +956,7 @@
file2.py: 23 lines
""")
- def test_debug_data_with_no_data_file(self):
+ def test_debug_data_with_no_data_file(self) -> None:
data = self.make_data_file()
self.command_line("debug data")
assert self.stdout() == textwrap.dedent(f"""\
@@ -933,7 +965,7 @@
No data collected: file doesn't exist
""")
- def test_debug_combinable_data(self):
+ def test_debug_combinable_data(self) -> None:
data1 = self.make_data_file(lines={"file1.py": range(1, 18), "file2.py": [1]})
data2 = self.make_data_file(suffix="123", lines={"file2.py": range(1, 10)})
@@ -956,13 +988,13 @@
class CmdLineStdoutTest(BaseCmdLineTest):
"""Test the command line with real stdout output."""
- def test_minimum_help(self):
+ def test_minimum_help(self) -> None:
self.command_line("")
out = self.stdout()
assert "Code coverage for Python" in out
assert out.count("\n") < 4
- def test_version(self):
+ def test_version(self) -> None:
self.command_line("--version")
out = self.stdout()
assert "ersion " in out
@@ -972,8 +1004,7 @@
assert "without C extension" in out
assert out.count("\n") < 4
- @pytest.mark.skipif(env.JYTHON, reason="Jython gets mad if you patch sys.argv")
- def test_help_contains_command_name(self):
+ def test_help_contains_command_name(self) -> None:
# Command name should be present in help output.
fake_command_path = os_sep("lorem/ipsum/dolor")
expected_command_name = "dolor"
@@ -983,8 +1014,7 @@
out = self.stdout()
assert expected_command_name in out
- @pytest.mark.skipif(env.JYTHON, reason="Jython gets mad if you patch sys.argv")
- def test_help_contains_command_name_from_package(self):
+ def test_help_contains_command_name_from_package(self) -> None:
# Command package name should be present in help output.
#
# When the main module is actually a package's `__main__` module, the resulting command line
@@ -999,13 +1029,13 @@
out = self.stdout()
assert expected_command_name in out
- def test_help(self):
+ def test_help(self) -> None:
self.command_line("help")
lines = self.stdout().splitlines()
assert len(lines) > 10
assert lines[-1] == f"Full documentation is at {__url__}"
- def test_cmd_help(self):
+ def test_cmd_help(self) -> None:
self.command_line("help run")
out = self.stdout()
lines = out.splitlines()
@@ -1014,26 +1044,26 @@
assert len(lines) > 20
assert lines[-1] == f"Full documentation is at {__url__}"
- def test_unknown_topic(self):
+ def test_unknown_topic(self) -> None:
# Should probably be an ERR return, but meh.
self.command_line("help foobar")
lines = self.stdout().splitlines()
assert lines[0] == "Don't know topic 'foobar'"
assert lines[-1] == f"Full documentation is at {__url__}"
- def test_error(self):
+ def test_error(self) -> None:
self.command_line("fooey kablooey", ret=ERR)
err = self.stderr()
assert "fooey" in err
assert "help" in err
- def test_option_error(self):
+ def test_option_error(self) -> None:
self.command_line("run --fooey", ret=ERR)
err = self.stderr()
assert "fooey" in err
assert "help" in err
- def test_doc_url(self):
+ def test_doc_url(self) -> None:
assert __url__.startswith("https://coverage.readthedocs.io")
@@ -1045,13 +1075,13 @@
class CoverageScriptStub:
"""A stub for coverage.cmdline.CoverageScript, used by CmdMainTest."""
- def command_line(self, argv):
+ def command_line(self, argv: List[str]) -> int:
"""Stub for command_line, the arg determines what it will do."""
if argv[0] == 'hello':
print("Hello, world!")
elif argv[0] == 'raise':
try:
- raise Exception("oh noes!")
+ raise RuntimeError("oh noes!")
except:
raise _ExceptionDuringRun(*sys.exc_info()) from None
elif argv[0] == 'internalraise':
@@ -1062,33 +1092,33 @@
raise AssertionError(f"Bad CoverageScriptStub: {argv!r}")
return 0
- def setUp(self):
+ def setUp(self) -> None:
super().setUp()
old_CoverageScript = coverage.cmdline.CoverageScript
- coverage.cmdline.CoverageScript = self.CoverageScriptStub
+ coverage.cmdline.CoverageScript = self.CoverageScriptStub # type: ignore
self.addCleanup(setattr, coverage.cmdline, 'CoverageScript', old_CoverageScript)
- def test_normal(self):
+ def test_normal(self) -> None:
ret = coverage.cmdline.main(['hello'])
assert ret == 0
assert self.stdout() == "Hello, world!\n"
- def test_raise(self):
+ def test_raise(self) -> None:
ret = coverage.cmdline.main(['raise'])
assert ret == 1
out, err = self.stdouterr()
assert out == ""
print(err)
- err = err.splitlines(keepends=True)
- assert err[0] == 'Traceback (most recent call last):\n'
- assert ' raise Exception("oh noes!")\n' in err
- assert err[-1] == 'Exception: oh noes!\n'
+ err_parts = err.splitlines(keepends=True)
+ assert err_parts[0] == 'Traceback (most recent call last):\n'
+ assert ' raise RuntimeError("oh noes!")\n' in err_parts
+ assert err_parts[-1] == 'RuntimeError: oh noes!\n'
- def test_internalraise(self):
+ def test_internalraise(self) -> None:
with pytest.raises(ValueError, match="coverage is broken"):
coverage.cmdline.main(['internalraise'])
- def test_exit(self):
+ def test_exit(self) -> None:
ret = coverage.cmdline.main(['exit'])
assert ret == 23
@@ -1096,7 +1126,14 @@
class CoverageReportingFake:
"""A fake Coverage.coverage test double for FailUnderTest methods."""
# pylint: disable=missing-function-docstring
- def __init__(self, report_result, html_result=0, xml_result=0, json_report=0, lcov_result=0):
+ def __init__(
+ self,
+ report_result: float,
+ html_result: float = 0,
+ xml_result: float = 0,
+ json_report: float = 0,
+ lcov_result: float = 0,
+ ) -> None:
self.config = CoverageConfig()
self.report_result = report_result
self.html_result = html_result
@@ -1104,28 +1141,28 @@
self.json_result = json_report
self.lcov_result = lcov_result
- def set_option(self, optname, optvalue):
+ def set_option(self, optname: str, optvalue: TConfigValueIn) -> None:
self.config.set_option(optname, optvalue)
- def get_option(self, optname):
+ def get_option(self, optname: str) -> TConfigValueOut:
return self.config.get_option(optname)
- def load(self):
+ def load(self) -> None:
pass
- def report(self, *args_unused, **kwargs_unused):
+ def report(self, *args_unused: Any, **kwargs_unused: Any) -> float:
return self.report_result
- def html_report(self, *args_unused, **kwargs_unused):
+ def html_report(self, *args_unused: Any, **kwargs_unused: Any) -> float:
return self.html_result
- def xml_report(self, *args_unused, **kwargs_unused):
+ def xml_report(self, *args_unused: Any, **kwargs_unused: Any) -> float:
return self.xml_result
- def json_report(self, *args_unused, **kwargs_unused):
+ def json_report(self, *args_unused: Any, **kwargs_unused: Any) -> float:
return self.json_result
- def lcov_report(self, *args_unused, **kwargs_unused):
+ def lcov_report(self, *args_unused: Any, **kwargs_unused: Any) -> float:
return self.lcov_result
@@ -1158,7 +1195,13 @@
# Command-line overrides configuration.
((20, 30, 40, 50, 60), 19, "report --fail-under=21", 2),
])
- def test_fail_under(self, results, fail_under, cmd, ret):
+ def test_fail_under(
+ self,
+ results: Tuple[float, float, float, float, float],
+ fail_under: Optional[float],
+ cmd: str,
+ ret: int,
+ ) -> None:
cov = CoverageReportingFake(*results)
if fail_under is not None:
cov.set_option("report:fail_under", fail_under)
@@ -1172,7 +1215,7 @@
(20.12345, "report --fail-under=20.1235 --precision=5", 2,
"Coverage failure: total of 20.12345 is less than fail-under=20.12350\n"),
])
- def test_fail_under_with_precision(self, result, cmd, ret, msg):
+ def test_fail_under_with_precision(self, result: float, cmd: str, ret: int, msg: str) -> None:
cov = CoverageReportingFake(report_result=result)
with mock.patch("coverage.cmdline.Coverage", lambda *a,**kw: cov):
self.command_line(cmd, ret)
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_collector.py python-coverage-7.2.7+dfsg1/tests/test_collector.py
--- python-coverage-6.5.0+dfsg1/tests/test_collector.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_collector.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,8 @@
"""Tests of coverage/collector.py and other collectors."""
+from __future__ import annotations
+
import os.path
import coverage
@@ -14,7 +16,7 @@
class CollectorTest(CoverageTest):
"""Test specific aspects of the collection process."""
- def test_should_trace_cache(self):
+ def test_should_trace_cache(self) -> None:
# The tracers should only invoke should_trace once for each file name.
# Make some files that invoke each other.
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_concurrency.py python-coverage-7.2.7+dfsg1/tests/test_concurrency.py
--- python-coverage-6.5.0+dfsg1/tests/test_concurrency.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_concurrency.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,14 +3,21 @@
"""Tests for concurrency libraries."""
+from __future__ import annotations
+
import glob
+import multiprocessing
import os
+import pathlib
import random
import re
import sys
import threading
import time
+from types import ModuleType
+from typing import Iterable, Optional
+
from flaky import flaky
import pytest
@@ -27,11 +34,6 @@
# These libraries aren't always available, we'll skip tests if they aren't.
try:
- import multiprocessing
-except ImportError: # pragma: only jython
- multiprocessing = None
-
-try:
import eventlet
except ImportError:
eventlet = None
@@ -43,11 +45,11 @@
try:
import greenlet
-except ImportError: # pragma: only jython
+except ImportError:
greenlet = None
-def measurable_line(l):
+def measurable_line(l: str) -> bool:
"""Is this a line of code coverage will measure?
Not blank, not a comment, and not "else"
@@ -59,18 +61,15 @@
return False
if l.startswith('else:'):
return False
- if env.JYTHON and l.startswith(('try:', 'except:', 'except ', 'break', 'with ')):
- # Jython doesn't measure these statements.
- return False # pragma: only jython
return True
-def line_count(s):
+def line_count(s: str) -> int:
"""How many measurable lines are in `s`?"""
return len(list(filter(measurable_line, s.splitlines())))
-def print_simple_annotation(code, linenos):
+def print_simple_annotation(code: str, linenos: Iterable[int]) -> None:
"""Print the lines in `code` with X for each line number in `linenos`."""
for lineno, line in enumerate(code.splitlines(), start=1):
print(" {} {}".format("X" if lineno in linenos else " ", line))
@@ -81,7 +80,7 @@
run_in_temp_dir = False
- def test_line_count(self):
+ def test_line_count(self) -> None:
CODE = """
# Hey there!
x = 1
@@ -175,7 +174,7 @@
"""
-def cant_trace_msg(concurrency, the_module):
+def cant_trace_msg(concurrency: str, the_module: Optional[ModuleType]) -> Optional[str]:
"""What might coverage.py say about a concurrency setting and imported module?"""
# In the concurrency choices, "multiprocessing" doesn't count, so remove it.
if "multiprocessing" in concurrency:
@@ -203,7 +202,13 @@
QLIMIT = 1000
- def try_some_code(self, code, concurrency, the_module, expected_out=None):
+ def try_some_code(
+ self,
+ code: str,
+ concurrency: str,
+ the_module: ModuleType,
+ expected_out: Optional[str] = None,
+ ) -> None:
"""Run some concurrency testing code and see that it was all covered.
`code` is the Python code to execute. `concurrency` is the name of
@@ -238,39 +243,40 @@
# If the test fails, it's helpful to see this info:
fname = abs_file("try_it.py")
linenos = data.lines(fname)
+ assert linenos is not None
print(f"{len(linenos)}: {linenos}")
print_simple_annotation(code, linenos)
lines = line_count(code)
assert line_counts(data)['try_it.py'] == lines
- def test_threads(self):
+ def test_threads(self) -> None:
code = (THREAD + SUM_RANGE_Q + PRINT_SUM_RANGE).format(QLIMIT=self.QLIMIT)
self.try_some_code(code, "thread", threading)
- def test_threads_simple_code(self):
+ def test_threads_simple_code(self) -> None:
code = SIMPLE.format(QLIMIT=self.QLIMIT)
self.try_some_code(code, "thread", threading)
- def test_eventlet(self):
+ def test_eventlet(self) -> None:
code = (EVENTLET + SUM_RANGE_Q + PRINT_SUM_RANGE).format(QLIMIT=self.QLIMIT)
self.try_some_code(code, "eventlet", eventlet)
- def test_eventlet_simple_code(self):
+ def test_eventlet_simple_code(self) -> None:
code = SIMPLE.format(QLIMIT=self.QLIMIT)
self.try_some_code(code, "eventlet", eventlet)
# https://github.com/nedbat/coveragepy/issues/663
@pytest.mark.skipif(env.WINDOWS, reason="gevent has problems on Windows: #663")
- def test_gevent(self):
+ def test_gevent(self) -> None:
code = (GEVENT + SUM_RANGE_Q + PRINT_SUM_RANGE).format(QLIMIT=self.QLIMIT)
self.try_some_code(code, "gevent", gevent)
- def test_gevent_simple_code(self):
+ def test_gevent_simple_code(self) -> None:
code = SIMPLE.format(QLIMIT=self.QLIMIT)
self.try_some_code(code, "gevent", gevent)
- def test_greenlet(self):
+ def test_greenlet(self) -> None:
GREENLET = """\
from greenlet import greenlet
@@ -288,11 +294,11 @@
"""
self.try_some_code(GREENLET, "greenlet", greenlet, "hello world\n42\n")
- def test_greenlet_simple_code(self):
+ def test_greenlet_simple_code(self) -> None:
code = SIMPLE.format(QLIMIT=self.QLIMIT)
self.try_some_code(code, "greenlet", greenlet)
- def test_bug_330(self):
+ def test_bug_330(self) -> None:
BUG_330 = """\
from weakref import WeakKeyDictionary
import eventlet
@@ -310,7 +316,7 @@
"""
self.try_some_code(BUG_330, "eventlet", eventlet, "0\n")
- def test_threads_with_gevent(self):
+ def test_threads_with_gevent(self) -> None:
self.make_file("both.py", """\
import queue
import threading
@@ -351,25 +357,25 @@
last_line = self.squeezed_lines(out)[-1]
assert re.search(r"TOTAL \d+ 0 100%", last_line)
- def test_bad_concurrency(self):
+ def test_bad_concurrency(self) -> None:
with pytest.raises(ConfigError, match="Unknown concurrency choices: nothing"):
self.command_line("run --concurrency=nothing prog.py")
- def test_bad_concurrency_in_config(self):
+ def test_bad_concurrency_in_config(self) -> None:
self.make_file(".coveragerc", "[run]\nconcurrency = nothing\n")
with pytest.raises(ConfigError, match="Unknown concurrency choices: nothing"):
self.command_line("run prog.py")
- def test_no_multiple_light_concurrency(self):
+ def test_no_multiple_light_concurrency(self) -> None:
with pytest.raises(ConfigError, match="Conflicting concurrency settings: eventlet, gevent"):
self.command_line("run --concurrency=gevent,eventlet prog.py")
- def test_no_multiple_light_concurrency_in_config(self):
+ def test_no_multiple_light_concurrency_in_config(self) -> None:
self.make_file(".coveragerc", "[run]\nconcurrency = gevent, eventlet\n")
with pytest.raises(ConfigError, match="Conflicting concurrency settings: eventlet, gevent"):
self.command_line("run prog.py")
- def test_multiprocessing_needs_config_file(self):
+ def test_multiprocessing_needs_config_file(self) -> None:
with pytest.raises(ConfigError, match="multiprocessing requires a configuration file"):
self.command_line("run --concurrency=multiprocessing prog.py")
@@ -378,9 +384,9 @@
"""Tests of what happens if the requested concurrency isn't installed."""
@pytest.mark.parametrize("module", ["eventlet", "gevent", "greenlet"])
- def test_missing_module(self, module):
+ def test_missing_module(self, module: str) -> None:
self.make_file("prog.py", "a = 1")
- sys.modules[module] = None
+ sys.modules[module] = None # type: ignore[assignment]
msg = f"Couldn't trace with concurrency={module}, the module isn't installed."
with pytest.raises(ConfigError, match=msg):
self.command_line(f"run --concurrency={module} prog.py")
@@ -434,30 +440,29 @@
@pytest.fixture(params=["fork", "spawn"], name="start_method")
-def start_method_fixture(request):
+def start_method_fixture(request: pytest.FixtureRequest) -> str:
"""Parameterized fixture to choose the start_method for multiprocessing."""
- start_method = request.param
+ start_method: str = request.param
if start_method not in multiprocessing.get_all_start_methods():
# Windows doesn't support "fork".
pytest.skip(f"start_method={start_method} not supported here")
return start_method
-@pytest.mark.skipif(not multiprocessing, reason="No multiprocessing in this Python")
@flaky(max_runs=30) # Sometimes a test fails due to inherent randomness. Try more times.
class MultiprocessingTest(CoverageTest):
"""Test support of the multiprocessing module."""
def try_multiprocessing_code(
self,
- code,
- expected_out,
- the_module,
- nprocs,
- start_method,
- concurrency="multiprocessing",
- args="",
- ):
+ code: str,
+ expected_out: Optional[str],
+ the_module: ModuleType,
+ nprocs: int,
+ start_method: str,
+ concurrency: str = "multiprocessing",
+ args: str = "",
+ ) -> None:
"""Run code using multiprocessing, it should produce `expected_out`."""
self.make_file("multi.py", code)
self.make_file(".coveragerc", f"""\
@@ -466,9 +471,7 @@
source = .
""")
- cmd = "coverage run {args} multi.py {start_method}".format(
- args=args, start_method=start_method,
- )
+ cmd = f"coverage run {args} multi.py {start_method}"
out = self.run_command(cmd)
expected_cant_trace = cant_trace_msg(concurrency, the_module)
@@ -484,15 +487,19 @@
out_lines = out.splitlines()
assert len(out_lines) == nprocs + 1
assert all(
- re.fullmatch(r"Combined data file \.coverage\..*\.\d+\.\d+", line)
+ re.fullmatch(
+ r"(Combined data file|Skipping duplicate data) \.coverage\..*\.\d+\.\d+",
+ line
+ )
for line in out_lines
)
+ assert len(glob.glob(".coverage.*")) == 0
out = self.run_command("coverage report -m")
last_line = self.squeezed_lines(out)[-1]
assert re.search(r"TOTAL \d+ 0 100%", last_line)
- def test_multiprocessing_simple(self, start_method):
+ def test_multiprocessing_simple(self, start_method: str) -> None:
nprocs = 3
upto = 30
code = (SQUARE_OR_CUBE_WORK + MULTI_CODE).format(NPROCS=nprocs, UPTO=upto)
@@ -506,7 +513,7 @@
start_method=start_method,
)
- def test_multiprocessing_append(self, start_method):
+ def test_multiprocessing_append(self, start_method: str) -> None:
nprocs = 3
upto = 30
code = (SQUARE_OR_CUBE_WORK + MULTI_CODE).format(NPROCS=nprocs, UPTO=upto)
@@ -521,7 +528,7 @@
start_method=start_method,
)
- def test_multiprocessing_and_gevent(self, start_method):
+ def test_multiprocessing_and_gevent(self, start_method: str) -> None:
nprocs = 3
upto = 30
code = (
@@ -538,7 +545,7 @@
start_method=start_method,
)
- def test_multiprocessing_with_branching(self, start_method):
+ def test_multiprocessing_with_branching(self, start_method: str) -> None:
nprocs = 3
upto = 30
code = (SQUARE_OR_CUBE_WORK + MULTI_CODE).format(NPROCS=nprocs, UPTO=upto)
@@ -562,7 +569,7 @@
last_line = self.squeezed_lines(out)[-1]
assert re.search(r"TOTAL \d+ 0 \d+ 0 100%", last_line)
- def test_multiprocessing_bootstrap_error_handling(self):
+ def test_multiprocessing_bootstrap_error_handling(self) -> None:
# An exception during bootstrapping will be reported.
self.make_file("multi.py", """\
import multiprocessing
@@ -577,9 +584,9 @@
""")
out = self.run_command("coverage run multi.py")
assert "Exception during multiprocessing bootstrap init" in out
- assert "Exception: Crashing because called by _bootstrap" in out
+ assert "RuntimeError: Crashing because called by _bootstrap" in out
- def test_bug_890(self):
+ def test_bug_890(self) -> None:
# chdir in multiprocessing shouldn't keep us from finding the
# .coveragerc file.
self.make_file("multi.py", """\
@@ -599,11 +606,11 @@
assert out.splitlines()[-1] == "ok"
-def test_coverage_stop_in_threads():
+def test_coverage_stop_in_threads() -> None:
has_started_coverage = []
has_stopped_coverage = []
- def run_thread(): # pragma: nested
+ def run_thread() -> None: # pragma: nested
"""Check that coverage is stopping properly in threads."""
deadline = time.time() + 5
ident = threading.current_thread().ident
@@ -630,27 +637,28 @@
assert has_stopped_coverage == [t.ident]
-def test_thread_safe_save_data(tmpdir):
+def test_thread_safe_save_data(tmp_path: pathlib.Path) -> None:
# Non-regression test for: https://github.com/nedbat/coveragepy/issues/581
# Create some Python modules and put them in the path
- modules_dir = tmpdir.mkdir('test_modules')
+ modules_dir = tmp_path / "test_modules"
+ modules_dir.mkdir()
module_names = [f"m{i:03d}" for i in range(1000)]
for module_name in module_names:
- modules_dir.join(module_name + ".py").write("def f(): pass\n")
+ (modules_dir / (module_name + ".py")).write_text("def f(): pass\n")
# Shared variables for threads
should_run = [True]
imported = []
old_dir = os.getcwd()
- os.chdir(modules_dir.strpath)
+ os.chdir(modules_dir)
try:
# Make sure that all dummy modules can be imported.
for module_name in module_names:
import_local_file(module_name)
- def random_load(): # pragma: nested
+ def random_load() -> None: # pragma: nested
"""Import modules randomly to stress coverage."""
while should_run[0]:
module_name = random.choice(module_names)
@@ -697,7 +705,7 @@
"""Tests of our handling of SIGTERM."""
@pytest.mark.parametrize("sigterm", [False, True])
- def test_sigterm_saves_data(self, sigterm):
+ def test_sigterm_multiprocessing_saves_data(self, sigterm: bool) -> None:
# A terminated process should save its coverage data.
self.make_file("clobbered.py", """\
import multiprocessing
@@ -743,7 +751,33 @@
expected = "clobbered.py 17 5 71% 5-10"
assert self.squeezed_lines(out)[2] == expected
- def test_sigterm_still_runs(self):
+ def test_sigterm_threading_saves_data(self) -> None:
+ # A terminated process should save its coverage data.
+ self.make_file("handler.py", """\
+ import os, signal
+
+ print("START", flush=True)
+ print("SIGTERM", flush=True)
+ os.kill(os.getpid(), signal.SIGTERM)
+ print("NOT HERE", flush=True)
+ """)
+ self.make_file(".coveragerc", """\
+ [run]
+ # The default concurrency option.
+ concurrency = thread
+ sigterm = true
+ """)
+ out = self.run_command("coverage run handler.py")
+ out_lines = out.splitlines()
+ assert len(out_lines) in [2, 3]
+ assert out_lines[:2] == ["START", "SIGTERM"]
+ if len(out_lines) == 3:
+ assert out_lines[2] == "Terminated"
+ out = self.run_command("coverage report -m")
+ expected = "handler.py 5 1 80% 6"
+ assert self.squeezed_lines(out)[2] == expected
+
+ def test_sigterm_still_runs(self) -> None:
# A terminated process still runs its own SIGTERM handler.
self.make_file("handler.py", """\
import multiprocessing
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_config.py python-coverage-7.2.7+dfsg1/tests/test_config.py
--- python-coverage-6.5.0+dfsg1/tests/test_config.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_config.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,32 +3,34 @@
"""Test the config file handling for coverage.py"""
-import math
-import sys
-from collections import OrderedDict
+from __future__ import annotations
+import sys
from unittest import mock
+
import pytest
import coverage
+from coverage import Coverage
from coverage.config import HandyConfigParser
from coverage.exceptions import ConfigError, CoverageWarning
+from coverage.tomlconfig import TomlConfigParser
+from coverage.types import FilePathClasses, FilePathType
from tests.coveragetest import CoverageTest, UsingModulesMixin
-from tests.helpers import without_module
class ConfigTest(CoverageTest):
"""Tests of the different sources of configuration settings."""
- def test_default_config(self):
+ def test_default_config(self) -> None:
# Just constructing a coverage() object gets the right defaults.
cov = coverage.Coverage()
assert not cov.config.timid
assert not cov.config.branch
assert cov.config.data_file == ".coverage"
- def test_arguments(self):
+ def test_arguments(self) -> None:
# Arguments to the constructor are applied to the configuration.
cov = coverage.Coverage(timid=True, data_file="fooey.dat", concurrency="multiprocessing")
assert cov.config.timid
@@ -36,7 +38,7 @@
assert cov.config.data_file == "fooey.dat"
assert cov.config.concurrency == ["multiprocessing"]
- def test_config_file(self):
+ def test_config_file(self) -> None:
# A .coveragerc file will be read into the configuration.
self.make_file(".coveragerc", """\
# This is just a bogus .rc file for testing.
@@ -49,7 +51,8 @@
assert not cov.config.branch
assert cov.config.data_file == ".hello_kitty.data"
- def test_named_config_file(self):
+ @pytest.mark.parametrize("file_class", FilePathClasses)
+ def test_named_config_file(self, file_class: FilePathType) -> None:
# You can name the config file what you like.
self.make_file("my_cov.ini", """\
[run]
@@ -57,13 +60,13 @@
; I wouldn't really use this as a data file...
data_file = delete.me
""")
- cov = coverage.Coverage(config_file="my_cov.ini")
+ cov = coverage.Coverage(config_file=file_class("my_cov.ini"))
assert cov.config.timid
assert not cov.config.branch
assert cov.config.data_file == "delete.me"
- def test_toml_config_file(self):
- # A .coveragerc file will be read into the configuration.
+ def test_toml_config_file(self) -> None:
+ # A pyproject.toml file will be read into the configuration.
self.make_file("pyproject.toml", """\
# This is just a bogus toml file for testing.
[tool.somethingelse]
@@ -81,7 +84,7 @@
[tool.coverage.plugins.a_plugin]
hello = "world"
""")
- cov = coverage.Coverage(config_file="pyproject.toml")
+ cov = coverage.Coverage()
assert cov.config.timid
assert not cov.config.branch
assert cov.config.concurrency == ["a", "b"]
@@ -89,20 +92,21 @@
assert cov.config.plugins == ["plugins.a_plugin"]
assert cov.config.precision == 3
assert cov.config.html_title == "tabblo & «ταБЬℓσ»"
- assert math.isclose(cov.config.fail_under, 90.5)
+ assert cov.config.fail_under == 90.5
assert cov.config.get_plugin_options("plugins.a_plugin") == {"hello": "world"}
+ def test_toml_ints_can_be_floats(self) -> None:
# Test that our class doesn't reject integers when loading floats
self.make_file("pyproject.toml", """\
# This is just a bogus toml file for testing.
[tool.coverage.report]
fail_under = 90
""")
- cov = coverage.Coverage(config_file="pyproject.toml")
- assert math.isclose(cov.config.fail_under, 90)
+ cov = coverage.Coverage()
+ assert cov.config.fail_under == 90
assert isinstance(cov.config.fail_under, float)
- def test_ignored_config_file(self):
+ def test_ignored_config_file(self) -> None:
# You can disable reading the .coveragerc file.
self.make_file(".coveragerc", """\
[run]
@@ -114,7 +118,7 @@
assert not cov.config.branch
assert cov.config.data_file == ".coverage"
- def test_config_file_then_args(self):
+ def test_config_file_then_args(self) -> None:
# The arguments override the .coveragerc file.
self.make_file(".coveragerc", """\
[run]
@@ -126,7 +130,7 @@
assert not cov.config.branch
assert cov.config.data_file == ".mycov"
- def test_data_file_from_environment(self):
+ def test_data_file_from_environment(self) -> None:
# There's an environment variable for the data_file.
self.make_file(".coveragerc", """\
[run]
@@ -140,7 +144,7 @@
cov = coverage.Coverage(data_file="fromarg.dat")
assert cov.config.data_file == "fromarg.dat"
- def test_debug_from_environment(self):
+ def test_debug_from_environment(self) -> None:
self.make_file(".coveragerc", """\
[run]
debug = dataio, pids
@@ -149,7 +153,7 @@
cov = coverage.Coverage()
assert cov.config.debug == ["dataio", "pids", "callers", "fooey"]
- def test_rcfile_from_environment(self):
+ def test_rcfile_from_environment(self) -> None:
self.make_file("here.ini", """\
[run]
data_file = overthere.dat
@@ -158,7 +162,7 @@
cov = coverage.Coverage()
assert cov.config.data_file == "overthere.dat"
- def test_missing_rcfile_from_environment(self):
+ def test_missing_rcfile_from_environment(self) -> None:
self.set_environ("COVERAGE_RCFILE", "nowhere.ini")
msg = "Couldn't read 'nowhere.ini' as a config file"
with pytest.raises(ConfigError, match=msg):
@@ -179,7 +183,7 @@
r"'foo\*\*\*': " +
r"multiple repeat"),
])
- def test_parse_errors(self, bad_config, msg):
+ def test_parse_errors(self, bad_config: str, msg: str) -> None:
# Im-parsable values raise ConfigError, with details.
self.make_file(".coveragerc", bad_config)
with pytest.raises(ConfigError, match=msg):
@@ -200,15 +204,15 @@
r"multiple repeat"),
('[tool.coverage.run]\nconcurrency="foo"', "not a list"),
("[tool.coverage.report]\nprecision=1.23", "not an integer"),
- ('[tool.coverage.report]\nfail_under="s"', "not a float"),
+ ('[tool.coverage.report]\nfail_under="s"', "couldn't convert to a float"),
])
- def test_toml_parse_errors(self, bad_config, msg):
+ def test_toml_parse_errors(self, bad_config: str, msg: str) -> None:
# Im-parsable values raise ConfigError, with details.
self.make_file("pyproject.toml", bad_config)
with pytest.raises(ConfigError, match=msg):
coverage.Coverage()
- def test_environment_vars_in_config(self):
+ def test_environment_vars_in_config(self) -> None:
# Config files can have $envvars in them.
self.make_file(".coveragerc", """\
[run]
@@ -230,13 +234,15 @@
assert cov.config.branch is True
assert cov.config.exclude_list == ["the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"]
- def test_environment_vars_in_toml_config(self):
+ def test_environment_vars_in_toml_config(self) -> None:
# Config files can have $envvars in them.
self.make_file("pyproject.toml", """\
[tool.coverage.run]
data_file = "$DATA_FILE.fooey"
- branch = $BRANCH
+ branch = "$BRANCH"
[tool.coverage.report]
+ precision = "$DIGITS"
+ fail_under = "$FAIL_UNDER"
exclude_lines = [
"the_$$one",
"another${THING}",
@@ -244,16 +250,24 @@
"x${NOTHING}y",
"huh$${X}what",
]
+ [othersection]
+ # This reproduces the failure from https://github.com/nedbat/coveragepy/issues/1481
+ # When OTHER has a backslash that isn't a valid escape, like \\z (see below).
+ something = "if [ $OTHER ]; then printf '%s\\n' 'Hi'; fi"
""")
self.set_environ("BRANCH", "true")
+ self.set_environ("DIGITS", "3")
+ self.set_environ("FAIL_UNDER", "90.5")
self.set_environ("DATA_FILE", "hello-world")
self.set_environ("THING", "ZZZ")
+ self.set_environ("OTHER", "hi\\zebra")
cov = coverage.Coverage()
- assert cov.config.data_file == "hello-world.fooey"
assert cov.config.branch is True
+ assert cov.config.precision == 3
+ assert cov.config.data_file == "hello-world.fooey"
assert cov.config.exclude_list == ["the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"]
- def test_tilde_in_config(self):
+ def test_tilde_in_config(self) -> None:
# Config entries that are file paths can be tilde-expanded.
self.make_file(".coveragerc", """\
[run]
@@ -276,7 +290,7 @@
~/src
~joe/source
""")
- def expanduser(s):
+ def expanduser(s: str) -> str:
"""Fake tilde expansion"""
s = s.replace("~/", "/Users/me/")
s = s.replace("~joe/", "/Users/joe/")
@@ -290,7 +304,7 @@
assert cov.config.exclude_list == ["~/data.file", "~joe/html_dir"]
assert cov.config.paths == {'mapping': ['/Users/me/src', '/Users/joe/source']}
- def test_tilde_in_toml_config(self):
+ def test_tilde_in_toml_config(self) -> None:
# Config entries that are file paths can be tilde-expanded.
self.make_file("pyproject.toml", """\
[tool.coverage.run]
@@ -309,7 +323,7 @@
"~joe/html_dir",
]
""")
- def expanduser(s):
+ def expanduser(s: str) -> str:
"""Fake tilde expansion"""
s = s.replace("~/", "/Users/me/")
s = s.replace("~joe/", "/Users/joe/")
@@ -322,7 +336,7 @@
assert cov.config.xml_output == "/Users/me/somewhere/xml.out"
assert cov.config.exclude_list == ["~/data.file", "~joe/html_dir"]
- def test_tweaks_after_constructor(self):
+ def test_tweaks_after_constructor(self) -> None:
# set_option can be used after construction to affect the config.
cov = coverage.Coverage(timid=True, data_file="fooey.dat")
cov.set_option("run:timid", False)
@@ -335,7 +349,7 @@
assert not cov.get_option("run:branch")
assert cov.get_option("run:data_file") == "fooey.dat"
- def test_tweaks_paths_after_constructor(self):
+ def test_tweaks_paths_after_constructor(self) -> None:
self.make_file(".coveragerc", """\
[paths]
first =
@@ -346,20 +360,22 @@
/second/a
/second/b
""")
- old_paths = OrderedDict()
- old_paths["first"] = ["/first/1", "/first/2"]
- old_paths["second"] = ["/second/a", "/second/b"]
+ old_paths = {
+ "first": ["/first/1", "/first/2"],
+ "second": ["/second/a", "/second/b"],
+ }
cov = coverage.Coverage()
paths = cov.get_option("paths")
assert paths == old_paths
- new_paths = OrderedDict()
- new_paths['magic'] = ['src', 'ok']
+ new_paths = {
+ "magic": ["src", "ok"],
+ }
cov.set_option("paths", new_paths)
assert cov.get_option("paths") == new_paths
- def test_tweak_error_checking(self):
+ def test_tweak_error_checking(self) -> None:
# Trying to set an unknown config value raises an error.
cov = coverage.Coverage()
with pytest.raises(ConfigError, match="No such option: 'run:xyzzy'"):
@@ -371,7 +387,7 @@
with pytest.raises(ConfigError, match="No such option: 'xyzzy:foo'"):
_ = cov.get_option("xyzzy:foo")
- def test_tweak_plugin_options(self):
+ def test_tweak_plugin_options(self) -> None:
# Plugin options have a more flexible syntax.
cov = coverage.Coverage()
cov.set_option("run:plugins", ["fooey.plugin", "xyzzy.coverage.plugin"])
@@ -385,7 +401,7 @@
with pytest.raises(ConfigError, match="No such option: 'no_such.plugin:foo'"):
_ = cov.get_option("no_such.plugin:foo")
- def test_unknown_option(self):
+ def test_unknown_option(self) -> None:
self.make_file(".coveragerc", """\
[run]
xyzzy = 17
@@ -394,7 +410,7 @@
with pytest.warns(CoverageWarning, match=msg):
_ = coverage.Coverage()
- def test_unknown_option_toml(self):
+ def test_unknown_option_toml(self) -> None:
self.make_file("pyproject.toml", """\
[tool.coverage.run]
xyzzy = 17
@@ -403,7 +419,7 @@
with pytest.warns(CoverageWarning, match=msg):
_ = coverage.Coverage()
- def test_misplaced_option(self):
+ def test_misplaced_option(self) -> None:
self.make_file(".coveragerc", """\
[report]
branch = True
@@ -412,7 +428,7 @@
with pytest.warns(CoverageWarning, match=msg):
_ = coverage.Coverage()
- def test_unknown_option_in_other_ini_file(self):
+ def test_unknown_option_in_other_ini_file(self) -> None:
self.make_file("setup.cfg", """\
[coverage:run]
huh = what?
@@ -421,17 +437,28 @@
with pytest.warns(CoverageWarning, match=msg):
_ = coverage.Coverage()
- def test_exceptions_from_missing_things(self):
+ def test_exceptions_from_missing_things(self) -> None:
self.make_file("config.ini", """\
[run]
branch = True
""")
- config = HandyConfigParser("config.ini")
+ config = HandyConfigParser(True)
+ config.read(["config.ini"])
with pytest.raises(ConfigError, match="No section: 'xyzzy'"):
config.options("xyzzy")
with pytest.raises(ConfigError, match="No option 'foo' in section: 'xyzzy'"):
config.get("xyzzy", "foo")
+ def test_exclude_also(self) -> None:
+ self.make_file("pyproject.toml", """\
+ [tool.coverage.report]
+ exclude_also = ["foobar", "raise .*Error"]
+ """)
+ cov = coverage.Coverage()
+
+ expected = coverage.config.DEFAULT_EXCLUDE + ["foobar", "raise .*Error"]
+ assert cov.config.exclude_list == expected
+
class ConfigFileTest(UsingModulesMixin, CoverageTest):
"""Tests of the config file settings in particular."""
@@ -482,6 +509,8 @@
skip_covered = TruE
skip_empty =TruE
+ include_namespace_packages = TRUE
+
[{section}html]
directory = c:\\tricky\\dir.somewhere
@@ -532,7 +561,7 @@
python igor.py zip_mods
"""
- def assert_config_settings_are_correct(self, cov):
+ def assert_config_settings_are_correct(self, cov: Coverage) -> None:
"""Check that `cov` has all the settings from LOTSA_SETTINGS."""
assert cov.config.timid
assert cov.config.data_file == "something_or_other.dat"
@@ -577,39 +606,40 @@
assert cov.config.get_plugin_options("plugins.another") == {}
assert cov.config.json_show_contexts is True
assert cov.config.json_pretty_print is True
+ assert cov.config.include_namespace_packages is True
- def test_config_file_settings(self):
+ def test_config_file_settings(self) -> None:
self.make_file(".coveragerc", self.LOTSA_SETTINGS.format(section=""))
cov = coverage.Coverage()
self.assert_config_settings_are_correct(cov)
- def check_config_file_settings_in_other_file(self, fname, contents):
+ def check_config_file_settings_in_other_file(self, fname: str, contents: str) -> None:
"""Check config will be read from another file, with prefixed sections."""
nested = self.LOTSA_SETTINGS.format(section="coverage:")
fname = self.make_file(fname, nested + "\n" + contents)
cov = coverage.Coverage()
self.assert_config_settings_are_correct(cov)
- def test_config_file_settings_in_setupcfg(self):
+ def test_config_file_settings_in_setupcfg(self) -> None:
self.check_config_file_settings_in_other_file("setup.cfg", self.SETUP_CFG)
- def test_config_file_settings_in_toxini(self):
+ def test_config_file_settings_in_toxini(self) -> None:
self.check_config_file_settings_in_other_file("tox.ini", self.TOX_INI)
- def check_other_config_if_coveragerc_specified(self, fname, contents):
+ def check_other_config_if_coveragerc_specified(self, fname: str, contents: str) -> None:
"""Check that config `fname` is read if .coveragerc is missing, but specified."""
nested = self.LOTSA_SETTINGS.format(section="coverage:")
self.make_file(fname, nested + "\n" + contents)
cov = coverage.Coverage(config_file=".coveragerc")
self.assert_config_settings_are_correct(cov)
- def test_config_file_settings_in_setupcfg_if_coveragerc_specified(self):
+ def test_config_file_settings_in_setupcfg_if_coveragerc_specified(self) -> None:
self.check_other_config_if_coveragerc_specified("setup.cfg", self.SETUP_CFG)
- def test_config_file_settings_in_tox_if_coveragerc_specified(self):
+ def test_config_file_settings_in_tox_if_coveragerc_specified(self) -> None:
self.check_other_config_if_coveragerc_specified("tox.ini", self.TOX_INI)
- def check_other_not_read_if_coveragerc(self, fname):
+ def check_other_not_read_if_coveragerc(self, fname: str) -> None:
"""Check config `fname` is not read if .coveragerc exists."""
self.make_file(".coveragerc", """\
[run]
@@ -622,16 +652,16 @@
""")
cov = coverage.Coverage()
assert cov.config.run_include == ["foo"]
- assert cov.config.run_omit is None
+ assert cov.config.run_omit == []
assert cov.config.branch is False
- def test_setupcfg_only_if_not_coveragerc(self):
+ def test_setupcfg_only_if_not_coveragerc(self) -> None:
self.check_other_not_read_if_coveragerc("setup.cfg")
- def test_toxini_only_if_not_coveragerc(self):
+ def test_toxini_only_if_not_coveragerc(self) -> None:
self.check_other_not_read_if_coveragerc("tox.ini")
- def check_other_config_need_prefixes(self, fname):
+ def check_other_config_need_prefixes(self, fname: str) -> None:
"""Check that `fname` sections won't be read if un-prefixed."""
self.make_file(fname, """\
[run]
@@ -639,16 +669,16 @@
branch = true
""")
cov = coverage.Coverage()
- assert cov.config.run_omit is None
+ assert cov.config.run_omit == []
assert cov.config.branch is False
- def test_setupcfg_only_if_prefixed(self):
+ def test_setupcfg_only_if_prefixed(self) -> None:
self.check_other_config_need_prefixes("setup.cfg")
- def test_toxini_only_if_prefixed(self):
+ def test_toxini_only_if_prefixed(self) -> None:
self.check_other_config_need_prefixes("tox.ini")
- def test_tox_ini_even_if_setup_cfg(self):
+ def test_tox_ini_even_if_setup_cfg(self) -> None:
# There's a setup.cfg, but no coverage settings in it, so tox.ini
# is read.
nested = self.LOTSA_SETTINGS.format(section="coverage:")
@@ -657,14 +687,14 @@
cov = coverage.Coverage()
self.assert_config_settings_are_correct(cov)
- def test_read_prefixed_sections_from_explicit_file(self):
+ def test_read_prefixed_sections_from_explicit_file(self) -> None:
# You can point to a tox.ini, and it will find [coverage:run] sections
nested = self.LOTSA_SETTINGS.format(section="coverage:")
self.make_file("tox.ini", self.TOX_INI + "\n" + nested)
cov = coverage.Coverage(config_file="tox.ini")
self.assert_config_settings_are_correct(cov)
- def test_non_ascii(self):
+ def test_non_ascii(self) -> None:
self.make_file(".coveragerc", """\
[report]
exclude_lines =
@@ -681,69 +711,86 @@
assert cov.config.html_title == "tabblo & «ταБЬℓσ» # numbers"
@pytest.mark.parametrize("bad_file", ["nosuchfile.txt", "."])
- def test_unreadable_config(self, bad_file):
+ def test_unreadable_config(self, bad_file: str) -> None:
# If a config file is explicitly specified, then it is an error for it
# to not be readable.
msg = f"Couldn't read {bad_file!r} as a config file"
with pytest.raises(ConfigError, match=msg):
coverage.Coverage(config_file=bad_file)
- def test_nocoveragerc_file_when_specified(self):
+ def test_nocoveragerc_file_when_specified(self) -> None:
cov = coverage.Coverage(config_file=".coveragerc")
assert not cov.config.timid
assert not cov.config.branch
assert cov.config.data_file == ".coverage"
- def test_note_is_obsolete(self):
- self.make_file("main.py", "a = 1")
- self.make_file(".coveragerc", """\
- [run]
- note = I am here I am here I am here!
- """)
- cov = coverage.Coverage()
- with self.assert_warnings(cov, [r"The '\[run] note' setting is no longer supported."]):
- self.start_import_stop(cov, "main")
- cov.report()
-
- def test_no_toml_installed_no_toml(self):
+ def test_no_toml_installed_no_toml(self) -> None:
# Can't read a toml file that doesn't exist.
- with without_module(coverage.tomlconfig, 'tomllib'):
+ with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
msg = "Couldn't read 'cov.toml' as a config file"
with pytest.raises(ConfigError, match=msg):
coverage.Coverage(config_file="cov.toml")
@pytest.mark.skipif(sys.version_info >= (3, 11), reason="Python 3.11 has toml in stdlib")
- def test_no_toml_installed_explicit_toml(self):
+ def test_no_toml_installed_explicit_toml(self) -> None:
# Can't specify a toml config file if toml isn't installed.
self.make_file("cov.toml", "# A toml file!")
- with without_module(coverage.tomlconfig, 'tomllib'):
+ with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
msg = "Can't read 'cov.toml' without TOML support"
with pytest.raises(ConfigError, match=msg):
coverage.Coverage(config_file="cov.toml")
@pytest.mark.skipif(sys.version_info >= (3, 11), reason="Python 3.11 has toml in stdlib")
- def test_no_toml_installed_pyproject_toml(self):
+ def test_no_toml_installed_pyproject_toml(self) -> None:
# Can't have coverage config in pyproject.toml without toml installed.
self.make_file("pyproject.toml", """\
# A toml file!
[tool.coverage.run]
xyzzy = 17
""")
- with without_module(coverage.tomlconfig, 'tomllib'):
+ with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
+ msg = "Can't read 'pyproject.toml' without TOML support"
+ with pytest.raises(ConfigError, match=msg):
+ coverage.Coverage()
+
+ @pytest.mark.skipif(sys.version_info >= (3, 11), reason="Python 3.11 has toml in stdlib")
+ def test_no_toml_installed_pyproject_toml_shorter_syntax(self) -> None:
+ # Can't have coverage config in pyproject.toml without toml installed.
+ self.make_file("pyproject.toml", """\
+ # A toml file!
+ [tool.coverage]
+ run.parallel = true
+ """)
+ with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
msg = "Can't read 'pyproject.toml' without TOML support"
with pytest.raises(ConfigError, match=msg):
coverage.Coverage()
- def test_no_toml_installed_pyproject_no_coverage(self):
+ @pytest.mark.skipif(sys.version_info >= (3, 11), reason="Python 3.11 has toml in stdlib")
+ def test_no_toml_installed_pyproject_no_coverage(self) -> None:
# It's ok to have non-coverage pyproject.toml without toml installed.
self.make_file("pyproject.toml", """\
# A toml file!
[tool.something]
xyzzy = 17
""")
- with without_module(coverage.tomlconfig, 'tomllib'):
+ with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
cov = coverage.Coverage()
# We get default settings:
assert not cov.config.timid
assert not cov.config.branch
assert cov.config.data_file == ".coverage"
+
+ def test_exceptions_from_missing_toml_things(self) -> None:
+ self.make_file("pyproject.toml", """\
+ [tool.coverage.run]
+ branch = true
+ """)
+ config = TomlConfigParser(False)
+ config.read("pyproject.toml")
+ with pytest.raises(ConfigError, match="No section: 'xyzzy'"):
+ config.options("xyzzy")
+ with pytest.raises(ConfigError, match="No section: 'xyzzy'"):
+ config.get("xyzzy", "foo")
+ with pytest.raises(ConfigError, match="No option 'foo' in section: 'tool.coverage.run'"):
+ config.get("run", "foo")
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_context.py python-coverage-7.2.7+dfsg1/tests/test_context.py
--- python-coverage-6.5.0+dfsg1/tests/test_context.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_context.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,13 +3,18 @@
"""Tests for context support."""
+from __future__ import annotations
+
import inspect
import os.path
+
+from typing import Any, List, Optional, Tuple
from unittest import mock
import coverage
from coverage.context import qualname_from_frame
-from coverage.data import CoverageData
+from coverage.data import CoverageData, sorted_lines
+from coverage.types import TArc, TCovKwargs, TLineNo
from tests.coveragetest import CoverageTest
from tests.helpers import assert_count_equal
@@ -18,14 +23,14 @@
class StaticContextTest(CoverageTest):
"""Tests of the static context."""
- def test_no_context(self):
+ def test_no_context(self) -> None:
self.make_file("main.py", "a = 1")
cov = coverage.Coverage()
self.start_import_stop(cov, "main")
data = cov.get_data()
assert_count_equal(data.measured_contexts(), [""])
- def test_static_context(self):
+ def test_static_context(self) -> None:
self.make_file("main.py", "a = 1")
cov = coverage.Coverage(context="gooey")
self.start_import_stop(cov, "main")
@@ -42,7 +47,7 @@
LINES = [1, 2, 4]
ARCS = [(-1, 1), (1, 2), (2, 4), (4, -1)]
- def run_red_blue(self, **options):
+ def run_red_blue(self, **options: TCovKwargs) -> Tuple[CoverageData, CoverageData]:
"""Run red.py and blue.py, and return their CoverageData objects."""
self.make_file("red.py", self.SOURCE)
red_cov = coverage.Coverage(context="red", data_suffix="r", source=["."], **options)
@@ -58,7 +63,7 @@
return red_data, blue_data
- def test_combining_line_contexts(self):
+ def test_combining_line_contexts(self) -> None:
red_data, blue_data = self.run_red_blue()
for datas in [[red_data, blue_data], [blue_data, red_data]]:
combined = CoverageData(suffix="combined")
@@ -73,7 +78,7 @@
fred = full_names['red.py']
fblue = full_names['blue.py']
- def assert_combined_lines(filename, context, lines):
+ def assert_combined_lines(filename: str, context: str, lines: List[TLineNo]) -> None:
# pylint: disable=cell-var-from-loop
combined.set_query_context(context)
assert combined.lines(filename) == lines
@@ -83,7 +88,7 @@
assert_combined_lines(fblue, 'red', [])
assert_combined_lines(fblue, 'blue', self.LINES)
- def test_combining_arc_contexts(self):
+ def test_combining_arc_contexts(self) -> None:
red_data, blue_data = self.run_red_blue(branch=True)
for datas in [[red_data, blue_data], [blue_data, red_data]]:
combined = CoverageData(suffix="combined")
@@ -98,7 +103,7 @@
fred = full_names['red.py']
fblue = full_names['blue.py']
- def assert_combined_lines(filename, context, lines):
+ def assert_combined_lines(filename: str, context: str, lines: List[TLineNo]) -> None:
# pylint: disable=cell-var-from-loop
combined.set_query_context(context)
assert combined.lines(filename) == lines
@@ -108,7 +113,7 @@
assert_combined_lines(fblue, 'red', [])
assert_combined_lines(fblue, 'blue', self.LINES)
- def assert_combined_arcs(filename, context, lines):
+ def assert_combined_arcs(filename: str, context: str, lines: List[TArc]) -> None:
# pylint: disable=cell-var-from-loop
combined.set_query_context(context)
assert combined.arcs(filename) == lines
@@ -149,7 +154,7 @@
TEST_ONE_LINES = [5, 6, 2]
TEST_TWO_LINES = [9, 10, 11, 13, 14, 15, 2]
- def test_dynamic_alone(self):
+ def test_dynamic_alone(self) -> None:
self.make_file("two_tests.py", self.SOURCE)
cov = coverage.Coverage(source=["."])
cov.set_option("run:dynamic_context", "test_function")
@@ -163,15 +168,15 @@
["", "two_tests.test_one", "two_tests.test_two"]
)
- def assert_context_lines(context, lines):
+ def assert_context_lines(context: str, lines: List[TLineNo]) -> None:
data.set_query_context(context)
- assert_count_equal(lines, data.lines(fname))
+ assert_count_equal(lines, sorted_lines(data, fname))
assert_context_lines("", self.OUTER_LINES)
assert_context_lines("two_tests.test_one", self.TEST_ONE_LINES)
assert_context_lines("two_tests.test_two", self.TEST_TWO_LINES)
- def test_static_and_dynamic(self):
+ def test_static_and_dynamic(self) -> None:
self.make_file("two_tests.py", self.SOURCE)
cov = coverage.Coverage(context="stat", source=["."])
cov.set_option("run:dynamic_context", "test_function")
@@ -185,33 +190,33 @@
["stat", "stat|two_tests.test_one", "stat|two_tests.test_two"]
)
- def assert_context_lines(context, lines):
+ def assert_context_lines(context: str, lines: List[TLineNo]) -> None:
data.set_query_context(context)
- assert_count_equal(lines, data.lines(fname))
+ assert_count_equal(lines, sorted_lines(data, fname))
assert_context_lines("stat", self.OUTER_LINES)
assert_context_lines("stat|two_tests.test_one", self.TEST_ONE_LINES)
assert_context_lines("stat|two_tests.test_two", self.TEST_TWO_LINES)
-def get_qualname():
+def get_qualname() -> Optional[str]:
"""Helper to return qualname_from_frame for the caller."""
stack = inspect.stack()[1:]
if any(sinfo[0].f_code.co_name == "get_qualname" for sinfo in stack):
# We're calling ourselves recursively, maybe because we're testing
# properties. Return an int to try to get back on track.
- return 17
+ return 17 # type: ignore[return-value]
caller_frame = stack[0][0]
return qualname_from_frame(caller_frame)
# pylint: disable=missing-class-docstring, missing-function-docstring, unused-argument
class Parent:
- def meth(self):
+ def meth(self) -> Optional[str]:
return get_qualname()
@property
- def a_property(self):
+ def a_property(self) -> Optional[str]:
return get_qualname()
class Child(Parent):
@@ -223,16 +228,16 @@
class MultiChild(SomethingElse, Child):
pass
-def no_arguments():
+def no_arguments() -> Optional[str]:
return get_qualname()
-def plain_old_function(a, b):
+def plain_old_function(a: Any, b: Any) -> Optional[str]:
return get_qualname()
-def fake_out(self):
+def fake_out(self: Any) -> Optional[str]:
return get_qualname()
-def patch_meth(self):
+def patch_meth(self: Any) -> Optional[str]:
return get_qualname()
# pylint: enable=missing-class-docstring, missing-function-docstring, unused-argument
@@ -246,38 +251,38 @@
run_in_temp_dir = False
- def test_method(self):
+ def test_method(self) -> None:
assert Parent().meth() == "tests.test_context.Parent.meth"
- def test_inherited_method(self):
+ def test_inherited_method(self) -> None:
assert Child().meth() == "tests.test_context.Parent.meth"
- def test_mi_inherited_method(self):
+ def test_mi_inherited_method(self) -> None:
assert MultiChild().meth() == "tests.test_context.Parent.meth"
- def test_no_arguments(self):
+ def test_no_arguments(self) -> None:
assert no_arguments() == "tests.test_context.no_arguments"
- def test_plain_old_function(self):
+ def test_plain_old_function(self) -> None:
assert plain_old_function(0, 1) == "tests.test_context.plain_old_function"
- def test_fake_out(self):
+ def test_fake_out(self) -> None:
assert fake_out(0) == "tests.test_context.fake_out"
- def test_property(self):
+ def test_property(self) -> None:
assert Parent().a_property == "tests.test_context.Parent.a_property"
- def test_changeling(self):
+ def test_changeling(self) -> None:
c = Child()
- c.meth = patch_meth
- assert c.meth(c) == "tests.test_context.patch_meth"
+ c.meth = patch_meth # type: ignore[assignment]
+ assert c.meth(c) == "tests.test_context.patch_meth" # type: ignore[call-arg]
- def test_bug_829(self):
+ def test_bug_829(self) -> None:
# A class with a name like a function shouldn't confuse qualname_from_frame.
class test_something: # pylint: disable=unused-variable
assert get_qualname() is None
- def test_bug_1210(self):
+ def test_bug_1210(self) -> None:
# Under pyarmor (an obfuscator), a function can have a "self" argument,
# but then not have a "self" local.
co = mock.Mock(co_name="a_co_name", co_argcount=1, co_varnames=["self"])
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_coverage.py python-coverage-7.2.7+dfsg1/tests/test_coverage.py
--- python-coverage-6.5.0+dfsg1/tests/test_coverage.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_coverage.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,8 @@
"""Tests for coverage.py."""
+from __future__ import annotations
+
import pytest
import coverage
@@ -10,13 +12,12 @@
from coverage.exceptions import NoDataError
from tests.coveragetest import CoverageTest
-from tests.helpers import xfail_pypy_3749
class TestCoverageTest(CoverageTest):
"""Make sure our complex self.check_coverage method works."""
- def test_successful_coverage(self):
+ def test_successful_coverage(self) -> None:
# The simplest run possible.
self.check_coverage("""\
a = 1
@@ -50,7 +51,7 @@
missing=("47-49", "3", "100,102")
)
- def test_failed_coverage(self):
+ def test_failed_coverage(self) -> None:
# If the lines are wrong, the message shows right and wrong.
with pytest.raises(AssertionError, match=r"\[1, 2] != \[1]"):
self.check_coverage("""\
@@ -90,7 +91,7 @@
missing=("37", "4-10"),
)
- def test_exceptions_really_fail(self):
+ def test_exceptions_really_fail(self) -> None:
# An assert in the checked code will really raise up to us.
with pytest.raises(AssertionError, match="This is bad"):
self.check_coverage("""\
@@ -111,7 +112,7 @@
class BasicCoverageTest(CoverageTest):
"""The simplest tests, for quick smoke testing of fundamental changes."""
- def test_simple(self):
+ def test_simple(self) -> None:
self.check_coverage("""\
a = 1
b = 2
@@ -122,7 +123,7 @@
""",
[1,2,4,6], report="4 0 0 0 100%")
- def test_indentation_wackiness(self):
+ def test_indentation_wackiness(self) -> None:
# Partial final lines are OK.
self.check_coverage("""\
import sys
@@ -131,7 +132,7 @@
""", # indented last line
[1,2,3], "3")
- def test_multiline_initializer(self):
+ def test_multiline_initializer(self) -> None:
self.check_coverage("""\
d = {
'foo': 1+2,
@@ -143,7 +144,7 @@
""",
[1,7], "")
- def test_list_comprehension(self):
+ def test_list_comprehension(self) -> None:
self.check_coverage("""\
l = [
2*i for i in range(10)
@@ -157,7 +158,7 @@
class SimpleStatementTest(CoverageTest):
"""Testing simple single-line statements."""
- def test_expression(self):
+ def test_expression(self) -> None:
# Bare expressions as statements are tricky: some implementations
# optimize some of them away. All implementations seem to count
# the implicit return at the end as executable.
@@ -186,7 +187,7 @@
""",
([1,2,4], [4]), "")
- def test_assert(self):
+ def test_assert(self) -> None:
self.check_coverage("""\
assert (1 + 2)
assert (1 +
@@ -198,7 +199,7 @@
""",
[1,2,4,5], "")
- def test_assignment(self):
+ def test_assignment(self) -> None:
# Simple variable assignment
self.check_coverage("""\
a = (1 + 2)
@@ -209,7 +210,7 @@
""",
[1,2,4], "")
- def test_assign_tuple(self):
+ def test_assign_tuple(self) -> None:
self.check_coverage("""\
a = 1
a,b,c = 7,8,9
@@ -217,7 +218,7 @@
""",
[1,2,3], "")
- def test_more_assignments(self):
+ def test_more_assignments(self) -> None:
self.check_coverage("""\
x = []
d = {}
@@ -232,7 +233,7 @@
""",
[1, 2, 3], "")
- def test_attribute_assignment(self):
+ def test_attribute_assignment(self) -> None:
# Attribute assignment
self.check_coverage("""\
class obj: pass
@@ -245,7 +246,7 @@
""",
[1,2,3,4,6], "")
- def test_list_of_attribute_assignment(self):
+ def test_list_of_attribute_assignment(self) -> None:
self.check_coverage("""\
class obj: pass
o = obj()
@@ -259,7 +260,7 @@
""",
[1,2,3,4,7], "")
- def test_augmented_assignment(self):
+ def test_augmented_assignment(self) -> None:
self.check_coverage("""\
a = 1
a += 1
@@ -270,7 +271,7 @@
""",
[1,2,3,5], "")
- def test_triple_string_stuff(self):
+ def test_triple_string_stuff(self) -> None:
self.check_coverage("""\
a = '''
a multiline
@@ -292,7 +293,7 @@
""",
[1,5,11], "")
- def test_pass(self):
+ def test_pass(self) -> None:
# pass is tricky: if it's the only statement in a block, then it is
# "executed". But if it is not the only statement, then it is not.
self.check_coverage("""\
@@ -329,7 +330,7 @@
""",
([1,2,4,5], [1,2,5]), "")
- def test_del(self):
+ def test_del(self) -> None:
self.check_coverage("""\
d = { 'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1 }
del d['a']
@@ -343,7 +344,7 @@
""",
[1,2,3,6,9], "")
- def test_raise(self):
+ def test_raise(self) -> None:
self.check_coverage("""\
try:
raise Exception(
@@ -354,7 +355,7 @@
""",
[1,2,5,6], "")
- def test_raise_followed_by_statement(self):
+ def test_raise_followed_by_statement(self) -> None:
if env.PYBEHAVIOR.omit_after_jump:
lines = [1,2,4,5]
missing = ""
@@ -370,7 +371,7 @@
""",
lines=lines, missing=missing)
- def test_return(self):
+ def test_return(self) -> None:
self.check_coverage("""\
def fn():
a = 1
@@ -403,7 +404,7 @@
""",
[1,2,3,7,8], "")
- def test_return_followed_by_statement(self):
+ def test_return_followed_by_statement(self) -> None:
if env.PYBEHAVIOR.omit_after_return:
lines = [1,2,3,6,7]
missing = ""
@@ -422,7 +423,7 @@
lines=lines, missing=missing,
)
- def test_yield(self):
+ def test_yield(self) -> None:
self.check_coverage("""\
def gen():
yield 1
@@ -436,7 +437,7 @@
""",
[1,2,3,6,8,9], "")
- def test_break(self):
+ def test_break(self) -> None:
if env.PYBEHAVIOR.omit_after_jump:
lines = [1,2,3,5]
missing = ""
@@ -453,7 +454,7 @@
""",
lines=lines, missing=missing)
- def test_continue(self):
+ def test_continue(self) -> None:
if env.PYBEHAVIOR.omit_after_jump:
lines = [1,2,3,5]
missing = ""
@@ -470,7 +471,7 @@
""",
lines=lines, missing=missing)
- def test_strange_unexecuted_continue(self):
+ def test_strange_unexecuted_continue(self) -> None:
# Peephole optimization of jumps to jumps can mean that some statements
# never hit the line tracer. The behavior is different in different
# versions of Python, so be careful when running this test.
@@ -501,7 +502,7 @@
missing=["", "6"],
)
- def test_import(self):
+ def test_import(self) -> None:
self.check_coverage("""\
import string
from sys import path
@@ -548,7 +549,7 @@
""",
[1,3], "")
- def test_global(self):
+ def test_global(self) -> None:
self.check_coverage("""\
g = h = i = 1
def fn():
@@ -569,7 +570,7 @@
""",
[1,2,3,4,5], "")
- def test_exec(self):
+ def test_exec(self) -> None:
self.check_coverage("""\
a = b = c = 1
exec("a = 2")
@@ -599,7 +600,7 @@
""",
[1,2,3,4,7], "")
- def test_extra_doc_string(self):
+ def test_extra_doc_string(self) -> None:
self.check_coverage("""\
a = 1
"An extra docstring, should be a comment."
@@ -622,7 +623,7 @@
"",
)
- def test_nonascii(self):
+ def test_nonascii(self) -> None:
self.check_coverage("""\
# coding: utf-8
a = 2
@@ -631,7 +632,7 @@
[2, 3]
)
- def test_module_docstring(self):
+ def test_module_docstring(self) -> None:
self.check_coverage("""\
'''I am a module docstring.'''
a = 2
@@ -653,7 +654,7 @@
class CompoundStatementTest(CoverageTest):
"""Testing coverage of multi-line compound statements."""
- def test_statement_list(self):
+ def test_statement_list(self) -> None:
self.check_coverage("""\
a = 1;
b = 2; c = 3
@@ -663,7 +664,7 @@
""",
[1,2,3,5], "")
- def test_if(self):
+ def test_if(self) -> None:
self.check_coverage("""\
a = 1
if a == 1:
@@ -706,7 +707,7 @@
""",
[1,2,3,4,6,8,9], "6-8")
- def test_elif(self):
+ def test_elif(self) -> None:
self.check_coverage("""\
a = 1; b = 2; c = 3;
if a == 1:
@@ -744,7 +745,7 @@
[1,2,3,4,5,7,8], "3, 5", report="7 2 4 2 64% 3, 5",
)
- def test_elif_no_else(self):
+ def test_elif_no_else(self) -> None:
self.check_coverage("""\
a = 1; b = 2; c = 3;
if a == 1:
@@ -766,7 +767,7 @@
[1,2,3,4,5,6], "3", report="6 1 4 2 70% 3, 4->6",
)
- def test_elif_bizarre(self):
+ def test_elif_bizarre(self) -> None:
self.check_coverage("""\
def f(self):
if self==1:
@@ -784,7 +785,7 @@
""",
[1,2,3,4,5,6,7,8,9,10,11,13], "2-13")
- def test_split_if(self):
+ def test_split_if(self) -> None:
self.check_coverage("""\
a = 1; b = 2; c = 3;
if \\
@@ -825,7 +826,7 @@
""",
[1,2,4,5,7,9,10], "4, 7")
- def test_pathological_split_if(self):
+ def test_pathological_split_if(self) -> None:
self.check_coverage("""\
a = 1; b = 2; c = 3;
if (
@@ -872,7 +873,7 @@
""",
[1,2,5,6,9,11,12], "5, 9")
- def test_absurd_split_if(self):
+ def test_absurd_split_if(self) -> None:
self.check_coverage("""\
a = 1; b = 2; c = 3;
if a == 1 \\
@@ -913,7 +914,7 @@
""",
[1,2,4,5,7,9,10], "4, 7")
- def test_constant_if(self):
+ def test_constant_if(self) -> None:
if env.PYBEHAVIOR.keep_constant_test:
lines = [1, 2, 3]
else:
@@ -927,7 +928,7 @@
"",
)
- def test_while(self):
+ def test_while(self) -> None:
self.check_coverage("""\
a = 3; b = 0
while a:
@@ -945,7 +946,7 @@
""",
[1,2,3,4,5], "")
- def test_while_else(self):
+ def test_while_else(self) -> None:
# Take the else branch.
self.check_coverage("""\
a = 3; b = 0
@@ -970,7 +971,7 @@
""",
[1,2,3,4,5,7,8], "7")
- def test_split_while(self):
+ def test_split_while(self) -> None:
self.check_coverage("""\
a = 3; b = 0
while \\
@@ -991,7 +992,7 @@
""",
[1,2,5,6,7], "")
- def test_for(self):
+ def test_for(self) -> None:
self.check_coverage("""\
a = 0
for i in [1,2,3,4,5]:
@@ -1017,7 +1018,7 @@
""",
[1,2,3,4,5], "")
- def test_for_else(self):
+ def test_for_else(self) -> None:
self.check_coverage("""\
a = 0
for i in range(5):
@@ -1038,7 +1039,7 @@
""",
[1,2,3,4,6,7], "6")
- def test_split_for(self):
+ def test_split_for(self) -> None:
self.check_coverage("""\
a = 0
for \\
@@ -1058,7 +1059,7 @@
""",
[1,2,6,7], "")
- def test_try_except(self):
+ def test_try_except(self) -> None:
self.check_coverage("""\
a = 0
try:
@@ -1119,8 +1120,8 @@
arcz_missing="45 58",
)
- def test_try_except_stranded_else(self):
- if env.PYBEHAVIOR.omit_after_jump:
+ def test_try_except_stranded_else(self) -> None:
+ if env.PYBEHAVIOR.optimize_unreachable_try_else:
# The else can't be reached because the try ends with a raise.
lines = [1,2,3,4,5,6,9]
missing = ""
@@ -1148,7 +1149,7 @@
arcz_missing=arcz_missing,
)
- def test_try_finally(self):
+ def test_try_finally(self) -> None:
self.check_coverage("""\
a = 0
try:
@@ -1172,7 +1173,7 @@
""",
[1,2,3,4,5,7,8,9,10], "")
- def test_function_def(self):
+ def test_function_def(self) -> None:
self.check_coverage("""\
a = 99
def foo():
@@ -1214,7 +1215,7 @@
""",
[1,10,12,13], "")
- def test_class_def(self):
+ def test_class_def(self) -> None:
arcz="-22 2D DE E-2 23 36 6A A-2 -68 8-6 -AB B-A"
self.check_coverage("""\
# A comment.
@@ -1240,7 +1241,7 @@
class ExcludeTest(CoverageTest):
"""Tests of the exclusion feature to mark lines as not covered."""
- def test_default(self):
+ def test_default(self) -> None:
# A number of forms of pragma comment are accepted.
self.check_coverage("""\
a = 1
@@ -1254,7 +1255,7 @@
[1,3,5,7]
)
- def test_simple(self):
+ def test_simple(self) -> None:
self.check_coverage("""\
a = 1; b = 2
@@ -1263,7 +1264,7 @@
""",
[1,3], "", excludes=['-cc'])
- def test_two_excludes(self):
+ def test_two_excludes(self) -> None:
self.check_coverage("""\
a = 1; b = 2
@@ -1275,7 +1276,7 @@
""",
[1,3,5,7], "5", excludes=['-cc', '-xx'])
- def test_excluding_if_suite(self):
+ def test_excluding_if_suite(self) -> None:
self.check_coverage("""\
a = 1; b = 2
@@ -1287,7 +1288,7 @@
""",
[1,7], "", excludes=['not-here'])
- def test_excluding_if_but_not_else_suite(self):
+ def test_excluding_if_but_not_else_suite(self) -> None:
self.check_coverage("""\
a = 1; b = 2
@@ -1302,7 +1303,7 @@
""",
[1,8,9,10], "", excludes=['not-here'])
- def test_excluding_else_suite(self):
+ def test_excluding_else_suite(self) -> None:
self.check_coverage("""\
a = 1; b = 2
@@ -1337,7 +1338,7 @@
""",
[1,3,4,5,6,17], "", excludes=['#pragma: NO COVER'])
- def test_excluding_elif_suites(self):
+ def test_excluding_elif_suites(self) -> None:
self.check_coverage("""\
a = 1; b = 2
@@ -1355,7 +1356,7 @@
""",
[1,3,4,5,6,11,12,13], "11-12", excludes=['#pragma: NO COVER'])
- def test_excluding_oneline_if(self):
+ def test_excluding_oneline_if(self) -> None:
self.check_coverage("""\
def foo():
a = 2
@@ -1366,7 +1367,7 @@
""",
[1,2,4,6], "", excludes=["no cover"])
- def test_excluding_a_colon_not_a_suite(self):
+ def test_excluding_a_colon_not_a_suite(self) -> None:
self.check_coverage("""\
def foo():
l = list(range(10))
@@ -1377,7 +1378,7 @@
""",
[1,2,4,6], "", excludes=["no cover"])
- def test_excluding_for_suite(self):
+ def test_excluding_for_suite(self) -> None:
self.check_coverage("""\
a = 0
for i in [1,2,3,4,5]: #pragma: NO COVER
@@ -1405,7 +1406,7 @@
""",
[1,7], "", excludes=['#pragma: NO COVER'])
- def test_excluding_for_else(self):
+ def test_excluding_for_else(self) -> None:
self.check_coverage("""\
a = 0
for i in range(5):
@@ -1417,7 +1418,7 @@
""",
[1,2,3,4,7], "", excludes=['#pragma: NO COVER'])
- def test_excluding_while(self):
+ def test_excluding_while(self) -> None:
self.check_coverage("""\
a = 3; b = 0
while a*b: #pragma: NO COVER
@@ -1437,7 +1438,7 @@
""",
[1,7], "", excludes=['#pragma: NO COVER'])
- def test_excluding_while_else(self):
+ def test_excluding_while_else(self) -> None:
self.check_coverage("""\
a = 3; b = 0
while a:
@@ -1449,7 +1450,7 @@
""",
[1,2,3,4,7], "", excludes=['#pragma: NO COVER'])
- def test_excluding_try_except(self):
+ def test_excluding_try_except(self) -> None:
self.check_coverage("""\
a = 0
try:
@@ -1496,8 +1497,8 @@
arcz_missing="58",
)
- def test_excluding_try_except_stranded_else(self):
- if env.PYBEHAVIOR.omit_after_jump:
+ def test_excluding_try_except_stranded_else(self) -> None:
+ if env.PYBEHAVIOR.optimize_unreachable_try_else:
# The else can't be reached because the try ends with a raise.
arcz = ".1 12 23 34 45 56 69 9."
arcz_missing = ""
@@ -1520,7 +1521,7 @@
arcz_missing=arcz_missing,
)
- def test_excluding_if_pass(self):
+ def test_excluding_if_pass(self) -> None:
# From a comment on the coverage.py page by Michael McNeil Forbes:
self.check_coverage("""\
def f():
@@ -1533,7 +1534,7 @@
""",
[1,7], "", excludes=["no cover"])
- def test_excluding_function(self):
+ def test_excluding_function(self) -> None:
self.check_coverage("""\
def fn(foo): #pragma: NO COVER
a = 1
@@ -1545,7 +1546,7 @@
""",
[6,7], "", excludes=['#pragma: NO COVER'])
- def test_excluding_method(self):
+ def test_excluding_method(self) -> None:
self.check_coverage("""\
class Fooey:
def __init__(self):
@@ -1559,7 +1560,7 @@
""",
[1,2,3,8,9], "", excludes=['#pragma: NO COVER'])
- def test_excluding_class(self):
+ def test_excluding_class(self) -> None:
self.check_coverage("""\
class Fooey: #pragma: NO COVER
def __init__(self):
@@ -1573,7 +1574,7 @@
""",
[8,9], "", excludes=['#pragma: NO COVER'])
- def test_excludes_non_ascii(self):
+ def test_excludes_non_ascii(self) -> None:
self.check_coverage("""\
# coding: utf-8
a = 1; b = 2
@@ -1584,7 +1585,7 @@
[2, 4], "", excludes=['✘cover']
)
- def test_formfeed(self):
+ def test_formfeed(self) -> None:
# https://github.com/nedbat/coveragepy/issues/461
self.check_coverage("""\
x = 1
@@ -1600,7 +1601,7 @@
[1, 6], "", excludes=['assert'],
)
- def test_excluded_comprehension_branches(self):
+ def test_excluded_comprehension_branches(self) -> None:
# https://github.com/nedbat/coveragepy/issues/1271
self.check_coverage("""\
x, y = [0], [1]
@@ -1618,8 +1619,7 @@
class Py24Test(CoverageTest):
"""Tests of new syntax in Python 2.4."""
- @xfail_pypy_3749
- def test_function_decorators(self):
+ def test_function_decorators(self) -> None:
lines = [1, 2, 3, 4, 6, 8, 10, 12]
if env.PYBEHAVIOR.trace_decorated_def:
lines = sorted(lines + [9])
@@ -1639,8 +1639,7 @@
""",
lines, "")
- @xfail_pypy_3749
- def test_function_decorators_with_args(self):
+ def test_function_decorators_with_args(self) -> None:
lines = [1, 2, 3, 4, 5, 6, 8, 10, 12]
if env.PYBEHAVIOR.trace_decorated_def:
lines = sorted(lines + [9])
@@ -1660,8 +1659,7 @@
""",
lines, "")
- @xfail_pypy_3749
- def test_double_function_decorators(self):
+ def test_double_function_decorators(self) -> None:
lines = [1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 14, 15, 17, 19, 21, 22, 24, 26]
if env.PYBEHAVIOR.trace_decorated_def:
lines = sorted(lines + [16, 23])
@@ -1699,7 +1697,7 @@
class Py25Test(CoverageTest):
"""Tests of new syntax in Python 2.5."""
- def test_with_statement(self):
+ def test_with_statement(self) -> None:
self.check_coverage("""\
class Managed:
def __enter__(self):
@@ -1722,7 +1720,7 @@
""",
[1,2,3,5,6,8,9,10,11,13,14,15,16,17,18], "")
- def test_try_except_finally(self):
+ def test_try_except_finally(self) -> None:
self.check_coverage("""\
a = 0; b = 0
try:
@@ -1802,8 +1800,8 @@
arcz_missing="45 59",
)
- def test_try_except_finally_stranded_else(self):
- if env.PYBEHAVIOR.omit_after_jump:
+ def test_try_except_finally_stranded_else(self) -> None:
+ if env.PYBEHAVIOR.optimize_unreachable_try_else:
# The else can't be reached because the try ends with a raise.
lines = [1,2,3,4,5,6,10,11]
missing = ""
@@ -1839,33 +1837,33 @@
run_in_temp_dir = False
- def test_not_singleton(self):
+ def test_not_singleton(self) -> None:
# You *can* create another coverage object.
coverage.Coverage()
coverage.Coverage()
- def test_old_name_and_new_name(self):
+ def test_old_name_and_new_name(self) -> None:
assert coverage.coverage is coverage.Coverage
class ReportingTest(CoverageTest):
"""Tests of some reporting behavior."""
- def test_no_data_to_report_on_annotate(self):
+ def test_no_data_to_report_on_annotate(self) -> None:
# Reporting with no data produces a nice message and no output
# directory.
with pytest.raises(NoDataError, match="No data to report."):
self.command_line("annotate -d ann")
self.assert_doesnt_exist("ann")
- def test_no_data_to_report_on_html(self):
+ def test_no_data_to_report_on_html(self) -> None:
# Reporting with no data produces a nice message and no output
# directory.
with pytest.raises(NoDataError, match="No data to report."):
self.command_line("html -d htmlcov")
self.assert_doesnt_exist("htmlcov")
- def test_no_data_to_report_on_xml(self):
+ def test_no_data_to_report_on_xml(self) -> None:
# Reporting with no data produces a nice message.
with pytest.raises(NoDataError, match="No data to report."):
self.command_line("xml")
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_data.py python-coverage-7.2.7+dfsg1/tests/test_data.py
--- python-coverage-6.5.0+dfsg1/tests/test_data.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_data.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,8 @@
"""Tests for coverage.data"""
+from __future__ import annotations
+
import glob
import os
import os.path
@@ -10,7 +12,11 @@
import sqlite3
import threading
+from typing import (
+ Any, Callable, Collection, Dict, Iterable, Mapping, Set, TypeVar, Union
+)
from unittest import mock
+
import pytest
from coverage.data import CoverageData, combine_parallel_data
@@ -18,6 +24,7 @@
from coverage.debug import DebugControlString
from coverage.exceptions import DataError, NoDataError
from coverage.files import PathAliases, canonical_filename
+from coverage.types import FilePathClasses, FilePathType, TArc, TLineNo
from tests.coveragetest import CoverageTest
from tests.helpers import assert_count_equal
@@ -58,7 +65,7 @@
MEASURED_FILES_3_4 = ['x.py', 'y.py', 'z.py']
-def DebugCoverageData(*args, **kwargs):
+def DebugCoverageData(*args: Any, **kwargs: Any) -> CoverageData:
"""Factory for CovergeData instances with debugging turned on.
This lets us exercise the debugging lines in sqldata.py. We don't make
@@ -73,25 +80,31 @@
# This is a way to get a mix of debug options across the tests.
options.extend(["sqldata"])
debug = DebugControlString(options=options)
- return CoverageData(*args, debug=debug, **kwargs)
+ return CoverageData(*args, debug=debug, **kwargs) # type: ignore[misc]
+
+TCoverageData = Callable[..., CoverageData]
-def assert_line_counts(covdata, counts, fullpath=False):
+def assert_line_counts(
+ covdata: CoverageData,
+ counts: Mapping[str, int],
+ fullpath: bool = False,
+) -> None:
"""Check that the line_counts of `covdata` is `counts`."""
assert line_counts(covdata, fullpath) == counts
-def assert_measured_files(covdata, measured):
+def assert_measured_files(covdata: CoverageData, measured: Iterable[str]) -> None:
"""Check that `covdata`'s measured files are `measured`."""
assert_count_equal(covdata.measured_files(), measured)
-def assert_lines1_data(covdata):
+def assert_lines1_data(covdata: CoverageData) -> None:
"""Check that `covdata` has the data from LINES1."""
assert_line_counts(covdata, SUMMARY_1)
assert_measured_files(covdata, MEASURED_FILES_1)
assert_count_equal(covdata.lines("a.py"), A_PY_LINES_1)
assert not covdata.has_arcs()
-def assert_arcs3_data(covdata):
+def assert_arcs3_data(covdata: CoverageData) -> None:
"""Check that `covdata` has the data from ARCS3."""
assert_line_counts(covdata, SUMMARY_3)
assert_measured_files(covdata, MEASURED_FILES_3)
@@ -102,7 +115,9 @@
assert covdata.has_arcs()
-def dicts_from_sets(file_data):
+TData = TypeVar("TData", bound=Union[TLineNo, TArc])
+
+def dicts_from_sets(file_data: Dict[str, Set[TData]]) -> Dict[str, Dict[TData, None]]:
"""Convert a dict of sets into a dict of dicts.
Before 6.0, file data was a dict with None as the values. In 6.0, file
@@ -115,65 +130,73 @@
class CoverageDataTest(CoverageTest):
"""Test cases for CoverageData."""
- def test_empty_data_is_false(self):
+ def test_empty_data_is_false(self) -> None:
covdata = DebugCoverageData()
assert not covdata
self.assert_doesnt_exist(".coverage")
- def test_empty_data_is_false_when_read(self):
+ def test_empty_data_is_false_when_read(self) -> None:
covdata = DebugCoverageData()
covdata.read()
assert not covdata
self.assert_doesnt_exist(".coverage")
- def test_line_data_is_true(self):
+ def test_line_data_is_true(self) -> None:
covdata = DebugCoverageData()
covdata.add_lines(LINES_1)
assert covdata
- def test_arc_data_is_true(self):
+ def test_arc_data_is_true(self) -> None:
covdata = DebugCoverageData()
covdata.add_arcs(ARCS_3)
assert covdata
- def test_empty_line_data_is_false(self):
+ def test_empty_line_data_is_false(self) -> None:
covdata = DebugCoverageData()
covdata.add_lines({})
assert not covdata
- def test_empty_arc_data_is_false(self):
+ def test_empty_arc_data_is_false(self) -> None:
covdata = DebugCoverageData()
covdata.add_arcs({})
assert not covdata
@pytest.mark.parametrize("lines", [LINES_1, dicts_from_sets(LINES_1)])
- def test_adding_lines(self, lines):
+ def test_adding_lines(self, lines: Mapping[str, Collection[TLineNo]]) -> None:
covdata = DebugCoverageData()
covdata.add_lines(lines)
assert_lines1_data(covdata)
@pytest.mark.parametrize("arcs", [ARCS_3, dicts_from_sets(ARCS_3)])
- def test_adding_arcs(self, arcs):
+ def test_adding_arcs(self, arcs: Mapping[str, Collection[TArc]]) -> None:
covdata = DebugCoverageData()
covdata.add_arcs(arcs)
assert_arcs3_data(covdata)
- def test_ok_to_add_lines_twice(self):
+ def test_ok_to_add_lines_twice(self) -> None:
covdata = DebugCoverageData()
covdata.add_lines(LINES_1)
covdata.add_lines(LINES_2)
assert_line_counts(covdata, SUMMARY_1_2)
assert_measured_files(covdata, MEASURED_FILES_1_2)
- def test_ok_to_add_arcs_twice(self):
+ def test_ok_to_add_arcs_twice(self) -> None:
+ covdata = DebugCoverageData()
+ covdata.add_arcs(ARCS_3)
+ covdata.add_arcs(ARCS_4)
+ assert_line_counts(covdata, SUMMARY_3_4)
+ assert_measured_files(covdata, MEASURED_FILES_3_4)
+
+ def test_ok_to_add_empty_arcs(self) -> None:
covdata = DebugCoverageData()
covdata.add_arcs(ARCS_3)
covdata.add_arcs(ARCS_4)
+ covdata.add_arcs(dict.fromkeys(ARCS_3, set()))
assert_line_counts(covdata, SUMMARY_3_4)
assert_measured_files(covdata, MEASURED_FILES_3_4)
@pytest.mark.parametrize("klass", [CoverageData, DebugCoverageData])
- def test_cant_add_arcs_with_lines(self, klass):
+ def test_cant_add_arcs_with_lines(self, klass: TCoverageData) -> None:
covdata = klass()
covdata.add_lines(LINES_1)
msg = "Can't add branch measurements to existing line data"
@@ -181,26 +204,26 @@
covdata.add_arcs(ARCS_3)
@pytest.mark.parametrize("klass", [CoverageData, DebugCoverageData])
- def test_cant_add_lines_with_arcs(self, klass):
+ def test_cant_add_lines_with_arcs(self, klass: TCoverageData) -> None:
covdata = klass()
covdata.add_arcs(ARCS_3)
msg = "Can't add line measurements to existing branch data"
with pytest.raises(DataError, match=msg):
covdata.add_lines(LINES_1)
- def test_touch_file_with_lines(self):
+ def test_touch_file_with_lines(self) -> None:
covdata = DebugCoverageData()
covdata.add_lines(LINES_1)
covdata.touch_file('zzz.py')
assert_measured_files(covdata, MEASURED_FILES_1 + ['zzz.py'])
- def test_touch_file_with_arcs(self):
+ def test_touch_file_with_arcs(self) -> None:
covdata = DebugCoverageData()
covdata.add_arcs(ARCS_3)
covdata.touch_file('zzz.py')
assert_measured_files(covdata, MEASURED_FILES_3 + ['zzz.py'])
- def test_set_query_contexts(self):
+ def test_set_query_contexts(self) -> None:
covdata = DebugCoverageData()
covdata.set_context('test_a')
covdata.add_lines(LINES_1)
@@ -209,14 +232,14 @@
covdata.set_query_contexts(['other'])
assert covdata.lines('a.py') == []
- def test_no_lines_vs_unmeasured_file(self):
+ def test_no_lines_vs_unmeasured_file(self) -> None:
covdata = DebugCoverageData()
covdata.add_lines(LINES_1)
covdata.touch_file('zzz.py')
assert covdata.lines('zzz.py') == []
assert covdata.lines('no_such_file.py') is None
- def test_lines_with_contexts(self):
+ def test_lines_with_contexts(self) -> None:
covdata = DebugCoverageData()
covdata.set_context('test_a')
covdata.add_lines(LINES_1)
@@ -226,7 +249,7 @@
covdata.set_query_contexts(['other'])
assert covdata.lines('a.py') == []
- def test_contexts_by_lineno_with_lines(self):
+ def test_contexts_by_lineno_with_lines(self) -> None:
covdata = DebugCoverageData()
covdata.set_context('test_a')
covdata.add_lines(LINES_1)
@@ -234,7 +257,7 @@
assert covdata.contexts_by_lineno('a.py') == expected
@pytest.mark.parametrize("lines", [LINES_1, dicts_from_sets(LINES_1)])
- def test_no_duplicate_lines(self, lines):
+ def test_no_duplicate_lines(self, lines: Mapping[str, Collection[TLineNo]]) -> None:
covdata = DebugCoverageData()
covdata.set_context("context1")
covdata.add_lines(lines)
@@ -243,7 +266,7 @@
assert covdata.lines('a.py') == A_PY_LINES_1
@pytest.mark.parametrize("arcs", [ARCS_3, dicts_from_sets(ARCS_3)])
- def test_no_duplicate_arcs(self, arcs):
+ def test_no_duplicate_arcs(self, arcs: Mapping[str, Collection[TArc]]) -> None:
covdata = DebugCoverageData()
covdata.set_context("context1")
covdata.add_arcs(arcs)
@@ -251,7 +274,7 @@
covdata.add_arcs(arcs)
assert covdata.arcs('x.py') == X_PY_ARCS_3
- def test_no_arcs_vs_unmeasured_file(self):
+ def test_no_arcs_vs_unmeasured_file(self) -> None:
covdata = DebugCoverageData()
covdata.add_arcs(ARCS_3)
covdata.touch_file('zzz.py')
@@ -260,7 +283,7 @@
assert covdata.arcs('zzz.py') == []
assert covdata.arcs('no_such_file.py') is None
- def test_arcs_with_contexts(self):
+ def test_arcs_with_contexts(self) -> None:
covdata = DebugCoverageData()
covdata.set_context('test_x')
covdata.add_arcs(ARCS_3)
@@ -270,20 +293,20 @@
covdata.set_query_contexts(['other'])
assert covdata.arcs('x.py') == []
- def test_contexts_by_lineno_with_arcs(self):
+ def test_contexts_by_lineno_with_arcs(self) -> None:
covdata = DebugCoverageData()
covdata.set_context('test_x')
covdata.add_arcs(ARCS_3)
expected = {1: ['test_x'], 2: ['test_x'], 3: ['test_x']}
assert covdata.contexts_by_lineno('x.py') == expected
- def test_contexts_by_lineno_with_unknown_file(self):
+ def test_contexts_by_lineno_with_unknown_file(self) -> None:
covdata = DebugCoverageData()
covdata.set_context('test_x')
covdata.add_arcs(ARCS_3)
assert covdata.contexts_by_lineno('xyz.py') == {}
- def test_context_by_lineno_with_query_contexts_with_lines(self):
+ def test_context_by_lineno_with_query_contexts_with_lines(self) -> None:
covdata = DebugCoverageData()
covdata.set_context("test_1")
covdata.add_lines(LINES_1)
@@ -292,7 +315,7 @@
covdata.set_query_context("test_1")
assert covdata.contexts_by_lineno("a.py") == dict.fromkeys([1,2], ["test_1"])
- def test_context_by_lineno_with_query_contexts_with_arcs(self):
+ def test_context_by_lineno_with_query_contexts_with_arcs(self) -> None:
covdata = DebugCoverageData()
covdata.set_context("test_1")
covdata.add_arcs(ARCS_3)
@@ -301,7 +324,7 @@
covdata.set_query_context("test_1")
assert covdata.contexts_by_lineno("x.py") == dict.fromkeys([1,2,3], ["test_1"])
- def test_file_tracer_name(self):
+ def test_file_tracer_name(self) -> None:
covdata = DebugCoverageData()
covdata.add_lines({
"p1.foo": [1, 2, 3],
@@ -314,7 +337,7 @@
assert covdata.file_tracer("main.py") == ""
assert covdata.file_tracer("p3.not_here") is None
- def test_ok_to_repeat_file_tracer(self):
+ def test_ok_to_repeat_file_tracer(self) -> None:
covdata = DebugCoverageData()
covdata.add_lines({
"p1.foo": [1, 2, 3],
@@ -324,7 +347,7 @@
covdata.add_file_tracers({"p1.foo": "p1.plugin"})
assert covdata.file_tracer("p1.foo") == "p1.plugin"
- def test_ok_to_set_empty_file_tracer(self):
+ def test_ok_to_set_empty_file_tracer(self) -> None:
covdata = DebugCoverageData()
covdata.add_lines({
"p1.foo": [1, 2, 3],
@@ -335,17 +358,7 @@
assert covdata.file_tracer("p1.foo") == "p1.plugin"
assert covdata.file_tracer("main.py") == ""
- def test_cant_file_tracer_unmeasured_files(self):
- covdata = DebugCoverageData()
- msg = "Can't add file tracer data for unmeasured file 'p1.foo'"
- with pytest.raises(DataError, match=msg):
- covdata.add_file_tracers({"p1.foo": "p1.plugin"})
-
- covdata.add_lines({"p2.html": [10, 11, 12]})
- with pytest.raises(DataError, match=msg):
- covdata.add_file_tracers({"p1.foo": "p1.plugin"})
-
- def test_cant_change_file_tracer_name(self):
+ def test_cant_change_file_tracer_name(self) -> None:
covdata = DebugCoverageData()
covdata.add_lines({"p1.foo": [1, 2, 3]})
covdata.add_file_tracers({"p1.foo": "p1.plugin"})
@@ -354,7 +367,7 @@
with pytest.raises(DataError, match=msg):
covdata.add_file_tracers({"p1.foo": "p1.plugin.foo"})
- def test_update_lines(self):
+ def test_update_lines(self) -> None:
covdata1 = DebugCoverageData(suffix='1')
covdata1.add_lines(LINES_1)
@@ -368,7 +381,7 @@
assert_line_counts(covdata3, SUMMARY_1_2)
assert_measured_files(covdata3, MEASURED_FILES_1_2)
- def test_update_arcs(self):
+ def test_update_arcs(self) -> None:
covdata1 = DebugCoverageData(suffix='1')
covdata1.add_arcs(ARCS_3)
@@ -382,7 +395,7 @@
assert_line_counts(covdata3, SUMMARY_3_4)
assert_measured_files(covdata3, MEASURED_FILES_3_4)
- def test_update_cant_mix_lines_and_arcs(self):
+ def test_update_cant_mix_lines_and_arcs(self) -> None:
covdata1 = DebugCoverageData(suffix='1')
covdata1.add_lines(LINES_1)
@@ -395,7 +408,7 @@
with pytest.raises(DataError, match="Can't combine line data with arc data"):
covdata2.update(covdata1)
- def test_update_file_tracers(self):
+ def test_update_file_tracers(self) -> None:
covdata1 = DebugCoverageData(suffix='1')
covdata1.add_lines({
"p1.html": [1, 2, 3, 4],
@@ -428,7 +441,7 @@
assert covdata3.file_tracer("p3.foo") == "foo_plugin"
assert covdata3.file_tracer("main.py") == ""
- def test_update_conflicting_file_tracers(self):
+ def test_update_conflicting_file_tracers(self) -> None:
covdata1 = DebugCoverageData(suffix='1')
covdata1.add_lines({"p1.html": [1, 2, 3]})
covdata1.add_file_tracers({"p1.html": "html.plugin"})
@@ -445,7 +458,7 @@
with pytest.raises(DataError, match=msg):
covdata2.update(covdata1)
- def test_update_file_tracer_vs_no_file_tracer(self):
+ def test_update_file_tracer_vs_no_file_tracer(self) -> None:
covdata1 = DebugCoverageData(suffix="1")
covdata1.add_lines({"p1.html": [1, 2, 3]})
covdata1.add_file_tracers({"p1.html": "html.plugin"})
@@ -461,7 +474,7 @@
with pytest.raises(DataError, match=msg):
covdata2.update(covdata1)
- def test_update_lines_empty(self):
+ def test_update_lines_empty(self) -> None:
covdata1 = DebugCoverageData(suffix='1')
covdata1.add_lines(LINES_1)
@@ -469,7 +482,7 @@
covdata1.update(covdata2)
assert_line_counts(covdata1, SUMMARY_1)
- def test_update_arcs_empty(self):
+ def test_update_arcs_empty(self) -> None:
covdata1 = DebugCoverageData(suffix='1')
covdata1.add_arcs(ARCS_3)
@@ -477,14 +490,14 @@
covdata1.update(covdata2)
assert_line_counts(covdata1, SUMMARY_3)
- def test_asking_isnt_measuring(self):
+ def test_asking_isnt_measuring(self) -> None:
# Asking about an unmeasured file shouldn't make it seem measured.
covdata = DebugCoverageData()
assert_measured_files(covdata, [])
assert covdata.arcs("missing.py") is None
assert_measured_files(covdata, [])
- def test_add_to_hash_with_lines(self):
+ def test_add_to_hash_with_lines(self) -> None:
covdata = DebugCoverageData()
covdata.add_lines(LINES_1)
hasher = mock.Mock()
@@ -494,7 +507,7 @@
mock.call.update(""), # file_tracer name
]
- def test_add_to_hash_with_arcs(self):
+ def test_add_to_hash_with_arcs(self) -> None:
covdata = DebugCoverageData()
covdata.add_arcs(ARCS_3)
covdata.add_file_tracers({"y.py": "hologram_plugin"})
@@ -505,7 +518,7 @@
mock.call.update("hologram_plugin"), # file_tracer name
]
- def test_add_to_lines_hash_with_missing_file(self):
+ def test_add_to_lines_hash_with_missing_file(self) -> None:
# https://github.com/nedbat/coveragepy/issues/403
covdata = DebugCoverageData()
covdata.add_lines(LINES_1)
@@ -516,7 +529,7 @@
mock.call.update(None),
]
- def test_add_to_arcs_hash_with_missing_file(self):
+ def test_add_to_arcs_hash_with_missing_file(self) -> None:
# https://github.com/nedbat/coveragepy/issues/403
covdata = DebugCoverageData()
covdata.add_arcs(ARCS_3)
@@ -528,25 +541,25 @@
mock.call.update(None),
]
- def test_empty_lines_are_still_lines(self):
+ def test_empty_lines_are_still_lines(self) -> None:
covdata = DebugCoverageData()
covdata.add_lines({})
covdata.touch_file("abc.py")
assert not covdata.has_arcs()
- def test_empty_arcs_are_still_arcs(self):
+ def test_empty_arcs_are_still_arcs(self) -> None:
covdata = DebugCoverageData()
covdata.add_arcs({})
covdata.touch_file("abc.py")
assert covdata.has_arcs()
- def test_cant_touch_in_empty_data(self):
+ def test_cant_touch_in_empty_data(self) -> None:
covdata = DebugCoverageData()
msg = "Can't touch files in an empty CoverageData"
with pytest.raises(DataError, match=msg):
covdata.touch_file("abc.py")
- def test_read_and_write_are_opposites(self):
+ def test_read_and_write_are_opposites(self) -> None:
covdata1 = DebugCoverageData()
covdata1.add_arcs(ARCS_3)
covdata1.write()
@@ -555,11 +568,11 @@
covdata2.read()
assert_arcs3_data(covdata2)
- def test_thread_stress(self):
+ def test_thread_stress(self) -> None:
covdata = DebugCoverageData()
exceptions = []
- def thread_main():
+ def thread_main() -> None:
"""Every thread will try to add the same data."""
try:
covdata.add_lines(LINES_1)
@@ -575,20 +588,52 @@
assert_lines1_data(covdata)
assert not exceptions
+ def test_purge_files_lines(self) -> None:
+ covdata = DebugCoverageData()
+ covdata.add_lines(LINES_1)
+ covdata.add_lines(LINES_2)
+ assert_line_counts(covdata, SUMMARY_1_2)
+ covdata.purge_files(["a.py", "b.py"])
+ assert_line_counts(covdata, {"a.py": 0, "b.py": 0, "c.py": 1})
+ covdata.purge_files(["c.py"])
+ assert_line_counts(covdata, {"a.py": 0, "b.py": 0, "c.py": 0})
+ # It's OK to "purge" a file that wasn't measured.
+ covdata.purge_files(["xyz.py"])
+ assert_line_counts(covdata, {"a.py": 0, "b.py": 0, "c.py": 0})
+
+ def test_purge_files_arcs(self) -> None:
+ covdata = CoverageData()
+ covdata.add_arcs(ARCS_3)
+ covdata.add_arcs(ARCS_4)
+ assert_line_counts(covdata, SUMMARY_3_4)
+ covdata.purge_files(["x.py", "y.py"])
+ assert_line_counts(covdata, {"x.py": 0, "y.py": 0, "z.py": 1})
+ covdata.purge_files(["z.py"])
+ assert_line_counts(covdata, {"x.py": 0, "y.py": 0, "z.py": 0})
+
+ def test_cant_purge_in_empty_data(self) -> None:
+ covdata = DebugCoverageData()
+ msg = "Can't purge files in an empty CoverageData"
+ with pytest.raises(DataError, match=msg):
+ covdata.purge_files(["abc.py"])
+
class CoverageDataInTempDirTest(CoverageTest):
"""Tests of CoverageData that need a temporary directory to make files."""
- def test_read_write_lines(self):
- covdata1 = DebugCoverageData("lines.dat")
+ @pytest.mark.parametrize("file_class", FilePathClasses)
+ def test_read_write_lines(self, file_class: FilePathType) -> None:
+ self.assert_doesnt_exist("lines.dat")
+ covdata1 = DebugCoverageData(file_class("lines.dat"))
covdata1.add_lines(LINES_1)
covdata1.write()
+ self.assert_exists("lines.dat")
covdata2 = DebugCoverageData("lines.dat")
covdata2.read()
assert_lines1_data(covdata2)
- def test_read_write_arcs(self):
+ def test_read_write_arcs(self) -> None:
covdata1 = DebugCoverageData("arcs.dat")
covdata1.add_arcs(ARCS_3)
covdata1.write()
@@ -597,14 +642,14 @@
covdata2.read()
assert_arcs3_data(covdata2)
- def test_read_errors(self):
+ def test_read_errors(self) -> None:
self.make_file("xyzzy.dat", "xyzzy")
with pytest.raises(DataError, match=r"Couldn't .* '.*[/\\]xyzzy.dat': \S+"):
covdata = DebugCoverageData("xyzzy.dat")
covdata.read()
assert not covdata
- def test_hard_read_error(self):
+ def test_hard_read_error(self) -> None:
self.make_file("noperms.dat", "go away")
os.chmod("noperms.dat", 0)
with pytest.raises(DataError, match=r"Couldn't .* '.*[/\\]noperms.dat': \S+"):
@@ -612,17 +657,17 @@
covdata.read()
@pytest.mark.parametrize("klass", [CoverageData, DebugCoverageData])
- def test_error_when_closing(self, klass):
+ def test_error_when_closing(self, klass: TCoverageData) -> None:
msg = r"Couldn't .* '.*[/\\]flaked.dat': \S+"
with pytest.raises(DataError, match=msg):
covdata = klass("flaked.dat")
covdata.add_lines(LINES_1)
# I don't know how to make a real error, so let's fake one.
sqldb = list(covdata._dbs.values())[0]
- sqldb.close = lambda: 1/0
+ sqldb.close = lambda: 1/0 # type: ignore[assignment]
covdata.add_lines(LINES_1)
- def test_wrong_schema_version(self):
+ def test_wrong_schema_version(self) -> None:
with sqlite3.connect("wrong_schema.db") as con:
con.execute("create table coverage_schema (version integer)")
con.execute("insert into coverage_schema (version) values (99)")
@@ -632,7 +677,7 @@
covdata.read()
assert not covdata
- def test_wrong_schema_schema(self):
+ def test_wrong_schema_schema(self) -> None:
with sqlite3.connect("wrong_schema_schema.db") as con:
con.execute("create table coverage_schema (xyzzy integer)")
con.execute("insert into coverage_schema (xyzzy) values (99)")
@@ -646,13 +691,13 @@
class CoverageDataFilesTest(CoverageTest):
"""Tests of CoverageData file handling."""
- def test_reading_missing(self):
+ def test_reading_missing(self) -> None:
self.assert_doesnt_exist(".coverage")
covdata = DebugCoverageData()
covdata.read()
assert_line_counts(covdata, {})
- def test_writing_and_reading(self):
+ def test_writing_and_reading(self) -> None:
covdata1 = DebugCoverageData()
covdata1.add_lines(LINES_1)
covdata1.write()
@@ -661,7 +706,7 @@
covdata2.read()
assert_line_counts(covdata2, SUMMARY_1)
- def test_debug_output_with_debug_option(self):
+ def test_debug_output_with_debug_option(self) -> None:
# With debug option dataio, we get debug output about reading and
# writing files.
debug = DebugControlString(options=["dataio"])
@@ -681,7 +726,7 @@
debug.get_output()
)
- def test_debug_output_without_debug_option(self):
+ def test_debug_output_without_debug_option(self) -> None:
# With a debug object, but not the dataio option, we don't get debug
# output.
debug = DebugControlString(options=[])
@@ -695,7 +740,7 @@
assert debug.get_output() == ""
- def test_explicit_suffix(self):
+ def test_explicit_suffix(self) -> None:
self.assert_doesnt_exist(".coverage.SUFFIX")
covdata = DebugCoverageData(suffix='SUFFIX')
covdata.add_lines(LINES_1)
@@ -703,7 +748,7 @@
self.assert_exists(".coverage.SUFFIX")
self.assert_doesnt_exist(".coverage")
- def test_true_suffix(self):
+ def test_true_suffix(self) -> None:
self.assert_file_count(".coverage.*", 0)
# suffix=True will make a randomly named data file.
@@ -725,7 +770,7 @@
# In addition to being different, the suffixes have the pid in them.
assert all(str(os.getpid()) in fn for fn in data_files2)
- def test_combining(self):
+ def test_combining(self) -> None:
self.assert_file_count(".coverage.*", 0)
covdata1 = DebugCoverageData(suffix='1')
@@ -746,7 +791,7 @@
assert_measured_files(covdata3, MEASURED_FILES_1_2)
self.assert_file_count(".coverage.*", 0)
- def test_erasing(self):
+ def test_erasing(self) -> None:
covdata1 = DebugCoverageData()
covdata1.add_lines(LINES_1)
covdata1.write()
@@ -758,7 +803,7 @@
covdata2.read()
assert_line_counts(covdata2, {})
- def test_erasing_parallel(self):
+ def test_erasing_parallel(self) -> None:
self.make_file("datafile.1")
self.make_file("datafile.2")
self.make_file(".coverage")
@@ -767,7 +812,7 @@
self.assert_file_count("datafile.*", 0)
self.assert_exists(".coverage")
- def test_combining_with_aliases(self):
+ def test_combining_with_aliases(self) -> None:
covdata1 = DebugCoverageData(suffix='1')
covdata1.add_lines({
'/home/ned/proj/src/a.py': {1, 2},
@@ -788,14 +833,16 @@
self.assert_file_count(".coverage.*", 2)
+ self.make_file("a.py", "")
+ self.make_file("sub/b.py", "")
+ self.make_file("template.html", "")
covdata3 = DebugCoverageData()
aliases = PathAliases()
aliases.add("/home/ned/proj/src/", "./")
aliases.add(r"c:\ned\test", "./")
combine_parallel_data(covdata3, aliases=aliases)
self.assert_file_count(".coverage.*", 0)
- # covdata3 hasn't been written yet. Should this file exist or not?
- #self.assert_exists(".coverage")
+ self.assert_exists(".coverage")
apy = canonical_filename('./a.py')
sub_bpy = canonical_filename('./sub/b.py')
@@ -805,7 +852,7 @@
assert_measured_files(covdata3, [apy, sub_bpy, template_html])
assert covdata3.file_tracer(template_html) == 'html.plugin'
- def test_combining_from_different_directories(self):
+ def test_combining_from_different_directories(self) -> None:
os.makedirs('cov1')
covdata1 = DebugCoverageData('cov1/.coverage.1')
covdata1.add_lines(LINES_1)
@@ -830,7 +877,7 @@
self.assert_doesnt_exist("cov2/.coverage.2")
self.assert_exists(".coverage.xxx")
- def test_combining_from_files(self):
+ def test_combining_from_files(self) -> None:
os.makedirs('cov1')
covdata1 = DebugCoverageData('cov1/.coverage.1')
covdata1.add_lines(LINES_1)
@@ -860,13 +907,13 @@
self.assert_exists(".coverage.xxx")
self.assert_exists("cov2/.coverage.xxx")
- def test_combining_from_nonexistent_directories(self):
+ def test_combining_from_nonexistent_directories(self) -> None:
covdata = DebugCoverageData()
msg = "Couldn't combine from non-existent path 'xyzzy'"
with pytest.raises(NoDataError, match=msg):
combine_parallel_data(covdata, data_paths=['xyzzy'])
- def test_interleaved_erasing_bug716(self):
+ def test_interleaved_erasing_bug716(self) -> None:
# pytest-cov could produce this scenario. #716
covdata1 = DebugCoverageData()
covdata2 = DebugCoverageData()
@@ -886,7 +933,7 @@
("[3-1]", "[b-a]"),
],
)
- def test_combining_with_crazy_filename(self, dpart, fpart):
+ def test_combining_with_crazy_filename(self, dpart: str, fpart: str) -> None:
dirname = f"py{dpart}"
basename = f"{dirname}/.coverage{fpart}"
os.makedirs(dirname)
@@ -905,6 +952,26 @@
assert_measured_files(covdata3, MEASURED_FILES_1_2)
self.assert_file_count(glob.escape(basename) + ".*", 0)
+ def test_meta_data(self) -> None:
+ # The metadata written to the data file shouldn't interfere with
+ # hashing to remove duplicates, except for debug=process, which
+ # writes debugging info as metadata.
+ debug = DebugControlString(options=[])
+ covdata1 = CoverageData(basename="meta.1", debug=debug)
+ covdata1.add_lines(LINES_1)
+ covdata1.write()
+ with sqlite3.connect("meta.1") as con:
+ data = sorted(k for (k,) in con.execute("select key from meta"))
+ assert data == ["has_arcs", "version"]
+
+ debug = DebugControlString(options=["process"])
+ covdata2 = CoverageData(basename="meta.2", debug=debug)
+ covdata2.add_lines(LINES_1)
+ covdata2.write()
+ with sqlite3.connect("meta.2") as con:
+ data = sorted(k for (k,) in con.execute("select key from meta"))
+ assert data == ["has_arcs", "sys_argv", "version", "when"]
+
class DumpsLoadsTest(CoverageTest):
"""Tests of CoverageData.dumps and loads."""
@@ -912,7 +979,7 @@
run_in_temp_dir = False
@pytest.mark.parametrize("klass", [CoverageData, DebugCoverageData])
- def test_serialization(self, klass):
+ def test_serialization(self, klass: TCoverageData) -> None:
covdata1 = klass(no_disk=True)
covdata1.add_lines(LINES_1)
covdata1.add_lines(LINES_2)
@@ -923,7 +990,7 @@
assert_line_counts(covdata2, SUMMARY_1_2)
assert_measured_files(covdata2, MEASURED_FILES_1_2)
- def test_misfed_serialization(self):
+ def test_misfed_serialization(self) -> None:
covdata = CoverageData(no_disk=True)
bad_data = b'Hello, world!\x07 ' + b'z' * 100
msg = r"Unrecognized serialization: {} \(head of {} bytes\)".format(
@@ -939,7 +1006,7 @@
run_in_temp_dir = False
- def test_updating(self):
+ def test_updating(self) -> None:
# https://github.com/nedbat/coveragepy/issues/1323
a = CoverageData(no_disk=True)
a.add_lines({'foo.py': [10, 20, 30]})
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_debug.py python-coverage-7.2.7+dfsg1/tests/test_debug.py
--- python-coverage-6.5.0+dfsg1/tests/test_debug.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_debug.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,18 +3,25 @@
"""Tests of coverage/debug.py"""
+from __future__ import annotations
+
import ast
import io
import os
import re
import sys
+from typing import Any, Callable, Iterable
+
import pytest
import coverage
from coverage import env
-from coverage.debug import filter_text, info_formatter, info_header, short_id, short_stack
-from coverage.debug import clipped_repr
+from coverage.debug import (
+ DebugOutputFile,
+ clipped_repr, filter_text, info_formatter, info_header, relevant_environment_display,
+ short_id, short_stack,
+)
from tests.coveragetest import CoverageTest
from tests.helpers import re_line, re_lines, re_lines_text
@@ -25,7 +32,7 @@
run_in_temp_dir = False
- def test_info_formatter(self):
+ def test_info_formatter(self) -> None:
lines = list(info_formatter([
('x', 'hello there'),
('very long label', ['one element']),
@@ -43,7 +50,7 @@
]
assert expected == lines
- def test_info_formatter_with_generator(self):
+ def test_info_formatter_with_generator(self) -> None:
lines = list(info_formatter(('info%d' % i, i) for i in range(3)))
expected = [
' info0: 0',
@@ -52,7 +59,7 @@
]
assert expected == lines
- def test_too_long_label(self):
+ def test_too_long_label(self) -> None:
with pytest.raises(AssertionError):
list(info_formatter([('this label is way too long and will not fit', 23)]))
@@ -61,7 +68,7 @@
("x", "-- x ---------------------------------------------------------"),
("hello there", "-- hello there -----------------------------------------------"),
])
-def test_info_header(label, header):
+def test_info_header(label: str, header: str) -> None:
assert info_header(label) == header
@@ -71,7 +78,7 @@
(0xA5A55A5A, 0xFFFF),
(0x1234cba956780fed, 0x8008),
])
-def test_short_id(id64, id16):
+def test_short_id(id64: int, id16: int) -> None:
assert short_id(id64) == id16
@@ -79,7 +86,7 @@
("hello", 10, "'hello'"),
("0123456789abcdefghijklmnopqrstuvwxyz", 15, "'01234...vwxyz'"),
])
-def test_clipped_repr(text, numchars, result):
+def test_clipped_repr(text: str, numchars: int, result: str) -> None:
assert clipped_repr(text, numchars) == result
@@ -90,14 +97,18 @@
("hello\nbye\n", [lambda x: "="+x], "=hello\n=bye\n"),
("hello\nbye\n", [lambda x: "="+x, lambda x: x+"\ndone\n"], "=hello\ndone\n=bye\ndone\n"),
])
-def test_filter_text(text, filters, result):
+def test_filter_text(
+ text: str,
+ filters: Iterable[Callable[[str], str]],
+ result: str,
+) -> None:
assert filter_text(text, filters) == result
class DebugTraceTest(CoverageTest):
"""Tests of debug output."""
- def f1_debug_output(self, debug):
+ def f1_debug_output(self, debug: Iterable[str]) -> str:
"""Runs some code with `debug` option, returns the debug output."""
# Make code to run.
self.make_file("f1.py", """\
@@ -116,13 +127,13 @@
return debug_out.getvalue()
- def test_debug_no_trace(self):
+ def test_debug_no_trace(self) -> None:
out_text = self.f1_debug_output([])
# We should have no output at all.
assert not out_text
- def test_debug_trace(self):
+ def test_debug_trace(self) -> None:
out_text = self.f1_debug_output(["trace"])
# We should have a line like "Tracing 'f1.py'", perhaps with an
@@ -132,7 +143,7 @@
# We should have lines like "Not tracing 'collector.py'..."
assert re_lines(r"^Not tracing .*: is part of coverage.py$", out_text)
- def test_debug_trace_pid(self):
+ def test_debug_trace_pid(self) -> None:
out_text = self.f1_debug_output(["trace", "pid"])
# Now our lines are always prefixed with the process id.
@@ -144,7 +155,7 @@
assert re_lines(pid_prefix + "Tracing ", out_text)
assert re_lines(pid_prefix + "Not tracing ", out_text)
- def test_debug_callers(self):
+ def test_debug_callers(self) -> None:
out_text = self.f1_debug_output(["pid", "dataop", "dataio", "callers", "lock"])
# For every real message, there should be a stack trace with a line like
# "f1_debug_output : /Users/ned/coverage/tests/test_debug.py @71"
@@ -161,7 +172,7 @@
assert re_lines(r"^\s*\d+\.\w{4}: Adding file tracers: 0 files", real_messages[-1])
assert re_lines(r"\s+add_file_tracers : .*coverage[/\\]sqldata.py:\d+$", last_line)
- def test_debug_config(self):
+ def test_debug_config(self) -> None:
out_text = self.f1_debug_output(["config"])
labels = """
@@ -176,21 +187,11 @@
msg = f"Incorrect lines for {label!r}"
assert 1 == len(re_lines(label_pat, out_text)), msg
- def test_debug_sys(self):
+ def test_debug_sys(self) -> None:
out_text = self.f1_debug_output(["sys"])
+ assert_good_debug_sys(out_text)
- labels = """
- coverage_version coverage_module coverage_paths stdlib_paths third_party_paths
- tracer configs_attempted config_file configs_read data_file
- python platform implementation executable
- pid cwd path environment command_line cover_match pylib_match
- """.split()
- for label in labels:
- label_pat = fr"^\s*{label}: "
- msg = f"Incorrect lines for {label!r}"
- assert 1 == len(re_lines(label_pat, out_text)), msg
-
- def test_debug_sys_ctracer(self):
+ def test_debug_sys_ctracer(self) -> None:
out_text = self.f1_debug_output(["sys"])
tracer_line = re_line(r"CTracer:", out_text).strip()
if env.C_TRACER:
@@ -199,7 +200,7 @@
expected = "CTracer: unavailable"
assert expected == tracer_line
- def test_debug_pybehave(self):
+ def test_debug_pybehave(self) -> None:
out_text = self.f1_debug_output(["pybehave"])
out_lines = out_text.splitlines()
assert 10 < len(out_lines) < 40
@@ -208,15 +209,72 @@
assert vtuple[:5] == sys.version_info
-def f_one(*args, **kwargs):
+def assert_good_debug_sys(out_text: str) -> None:
+ """Assert that `str` is good output for debug=sys."""
+ labels = """
+ coverage_version coverage_module coverage_paths stdlib_paths third_party_paths
+ tracer configs_attempted config_file configs_read data_file
+ python platform implementation executable
+ pid cwd path environment command_line cover_match pylib_match
+ """.split()
+ for label in labels:
+ label_pat = fr"^\s*{label}: "
+ msg = f"Incorrect lines for {label!r}"
+ assert 1 == len(re_lines(label_pat, out_text)), msg
+
+
+class DebugOutputTest(CoverageTest):
+ """Tests that we can direct debug output where we want."""
+
+ def setUp(self) -> None:
+ super().setUp()
+ # DebugOutputFile aggressively tries to start just one output file. We
+ # need to manually force it to make a new one.
+ DebugOutputFile._del_singleton_data()
+
+ def debug_sys(self) -> None:
+ """Run just enough coverage to get full debug=sys output."""
+ cov = coverage.Coverage(debug=["sys"])
+ cov.start()
+ cov.stop()
+
+ def test_stderr_default(self) -> None:
+ self.debug_sys()
+ out, err = self.stdouterr()
+ assert out == ""
+ assert_good_debug_sys(err)
+
+ def test_envvar(self) -> None:
+ self.set_environ("COVERAGE_DEBUG_FILE", "debug.out")
+ self.debug_sys()
+ assert self.stdouterr() == ("", "")
+ with open("debug.out") as f:
+ assert_good_debug_sys(f.read())
+
+ def test_config_file(self) -> None:
+ self.make_file(".coveragerc", "[run]\ndebug_file = lotsa_info.txt")
+ self.debug_sys()
+ assert self.stdouterr() == ("", "")
+ with open("lotsa_info.txt") as f:
+ assert_good_debug_sys(f.read())
+
+ def test_stdout_alias(self) -> None:
+ self.set_environ("COVERAGE_DEBUG_FILE", "stdout")
+ self.debug_sys()
+ out, err = self.stdouterr()
+ assert err == ""
+ assert_good_debug_sys(out)
+
+
+def f_one(*args: Any, **kwargs: Any) -> str:
"""First of the chain of functions for testing `short_stack`."""
return f_two(*args, **kwargs)
-def f_two(*args, **kwargs):
+def f_two(*args: Any, **kwargs: Any) -> str:
"""Second of the chain of functions for testing `short_stack`."""
return f_three(*args, **kwargs)
-def f_three(*args, **kwargs):
+def f_three(*args: Any, **kwargs: Any) -> str:
"""Third of the chain of functions for testing `short_stack`."""
return short_stack(*args, **kwargs)
@@ -226,17 +284,36 @@
run_in_temp_dir = False
- def test_short_stack(self):
+ def test_short_stack(self) -> None:
stack = f_one().splitlines()
assert len(stack) > 10
assert "f_three" in stack[-1]
assert "f_two" in stack[-2]
assert "f_one" in stack[-3]
- def test_short_stack_limit(self):
+ def test_short_stack_limit(self) -> None:
stack = f_one(limit=5).splitlines()
assert len(stack) == 5
- def test_short_stack_skip(self):
+ def test_short_stack_skip(self) -> None:
stack = f_one(skip=1).splitlines()
assert "f_two" in stack[-1]
+
+
+def test_relevant_environment_display() -> None:
+ env_vars = {
+ "HOME": "my home",
+ "HOME_DIR": "other place",
+ "XYZ_NEVER_MIND": "doesn't matter",
+ "SOME_PYOTHER": "xyz123",
+ "COVERAGE_THING": "abcd",
+ "MY_PYPI_TOKEN": "secret.something",
+ "TMP": "temporary",
+ }
+ assert relevant_environment_display(env_vars) == [
+ ("COVERAGE_THING", "abcd"),
+ ("HOME", "my home"),
+ ("MY_PYPI_TOKEN", "******.*********"),
+ ("SOME_PYOTHER", "xyz123"),
+ ("TMP", "temporary"),
+ ]
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_execfile.py python-coverage-7.2.7+dfsg1/tests/test_execfile.py
--- python-coverage-6.5.0+dfsg1/tests/test_execfile.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_execfile.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,8 @@
"""Tests for coverage.execfile"""
+from __future__ import annotations
+
import compileall
import json
import os
@@ -12,9 +14,10 @@
import re
import sys
+from typing import Any, Iterator
+
import pytest
-from coverage import env
from coverage.exceptions import NoCode, NoSource, _ExceptionDuringRun
from coverage.execfile import run_python_file, run_python_module
from coverage.files import python_reported_file
@@ -28,12 +31,12 @@
"""Test cases for `run_python_file`."""
@pytest.fixture(autouse=True)
- def clean_up(self):
+ def clean_up(self) -> Iterator[None]:
"""These tests all run in-process. Clean up global changes."""
yield
sys.excepthook = sys.__excepthook__
- def test_run_python_file(self):
+ def test_run_python_file(self) -> None:
run_python_file([TRY_EXECFILE, "arg1", "arg2"])
mod_globs = json.loads(self.stdout())
@@ -59,7 +62,7 @@
# __builtins__ should have the right values, like open().
assert mod_globs['__builtins__.has_open'] is True
- def test_no_extra_file(self):
+ def test_no_extra_file(self) -> None:
# Make sure that running a file doesn't create an extra compiled file.
self.make_file("xxx", """\
desc = "a non-.py file!"
@@ -69,7 +72,7 @@
run_python_file(["xxx"])
assert os.listdir(".") == ["xxx"]
- def test_universal_newlines(self):
+ def test_universal_newlines(self) -> None:
# Make sure we can read any sort of line ending.
pylines = """# try newlines|print('Hello, world!')|""".split('|')
for nl in ('\n', '\r\n', '\r'):
@@ -78,7 +81,7 @@
run_python_file(['nl.py'])
assert self.stdout() == "Hello, world!\n"*3
- def test_missing_final_newline(self):
+ def test_missing_final_newline(self) -> None:
# Make sure we can deal with a Python file with no final newline.
self.make_file("abrupt.py", """\
if 1:
@@ -91,25 +94,25 @@
run_python_file(["abrupt.py"])
assert self.stdout() == "a is 1\n"
- def test_no_such_file(self):
+ def test_no_such_file(self) -> None:
path = python_reported_file('xyzzy.py')
msg = re.escape(f"No file to run: '{path}'")
with pytest.raises(NoSource, match=msg):
run_python_file(["xyzzy.py"])
- def test_directory_with_main(self):
+ def test_directory_with_main(self) -> None:
self.make_file("with_main/__main__.py", """\
print("I am __main__")
""")
run_python_file(["with_main"])
assert self.stdout() == "I am __main__\n"
- def test_directory_without_main(self):
+ def test_directory_without_main(self) -> None:
self.make_file("without_main/__init__.py", "")
with pytest.raises(NoSource, match="Can't find '__main__' module in 'without_main'"):
run_python_file(["without_main"])
- def test_code_throws(self):
+ def test_code_throws(self) -> None:
self.make_file("throw.py", """\
class MyException(Exception):
pass
@@ -130,7 +133,7 @@
assert self.stdout() == "about to raise..\n"
assert self.stderr() == ""
- def test_code_exits(self):
+ def test_code_exits(self) -> None:
self.make_file("exit.py", """\
import sys
def f1():
@@ -149,7 +152,7 @@
assert self.stdout() == "about to exit..\n"
assert self.stderr() == ""
- def test_excepthook_exit(self):
+ def test_excepthook_exit(self) -> None:
self.make_file("excepthook_exit.py", """\
import sys
@@ -166,7 +169,7 @@
cov_out = self.stdout()
assert cov_out == "in excepthook\n"
- def test_excepthook_throw(self):
+ def test_excepthook_throw(self) -> None:
self.make_file("excepthook_throw.py", """\
import sys
@@ -194,11 +197,8 @@
class RunPycFileTest(CoverageTest):
"""Test cases for `run_python_file`."""
- def make_pyc(self, **kwargs):
+ def make_pyc(self, **kwargs: Any) -> str:
"""Create a .pyc file, and return the path to it."""
- if env.JYTHON:
- pytest.skip("Can't make .pyc files on Jython")
-
self.make_file("compiled.py", """\
def doit():
print("I am here!")
@@ -211,12 +211,12 @@
# Find the .pyc file!
return str(next(pathlib.Path(".").rglob("compiled*.pyc")))
- def test_running_pyc(self):
+ def test_running_pyc(self) -> None:
pycfile = self.make_pyc()
run_python_file([pycfile])
assert self.stdout() == "I am here!\n"
- def test_running_pyo(self):
+ def test_running_pyo(self) -> None:
pycfile = self.make_pyc()
pyofile = re.sub(r"[.]pyc$", ".pyo", pycfile)
assert pycfile != pyofile
@@ -224,7 +224,7 @@
run_python_file([pyofile])
assert self.stdout() == "I am here!\n"
- def test_running_pyc_from_wrong_python(self):
+ def test_running_pyc_from_wrong_python(self) -> None:
pycfile = self.make_pyc()
# Jam Python 2.1 magic number into the .pyc file.
@@ -238,18 +238,18 @@
# In some environments, the pycfile persists and pollutes another test.
os.remove(pycfile)
- def test_running_hashed_pyc(self):
+ def test_running_hashed_pyc(self) -> None:
pycfile = self.make_pyc(invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH)
run_python_file([pycfile])
assert self.stdout() == "I am here!\n"
- def test_no_such_pyc_file(self):
+ def test_no_such_pyc_file(self) -> None:
path = python_reported_file('xyzzy.pyc')
msg = re.escape(f"No file to run: '{path}'")
with pytest.raises(NoCode, match=msg):
run_python_file(["xyzzy.pyc"])
- def test_running_py_from_binary(self):
+ def test_running_py_from_binary(self) -> None:
# Use make_file to get the bookkeeping. Ideally, it would
# be able to write binary files.
bf = self.make_file("binary")
@@ -259,7 +259,7 @@
path = python_reported_file('binary')
msg = (
re.escape(f"Couldn't run '{path}' as Python code: ") +
- r"(TypeError|ValueError): source code string cannot contain null bytes"
+ r"(ValueError|SyntaxError): source code string cannot contain null bytes"
)
with pytest.raises(Exception, match=msg):
run_python_file([bf])
@@ -270,43 +270,43 @@
run_in_temp_dir = False
- def test_runmod1(self):
+ def test_runmod1(self) -> None:
run_python_module(["runmod1", "hello"])
out, err = self.stdouterr()
assert out == "runmod1: passed hello\n"
assert err == ""
- def test_runmod2(self):
+ def test_runmod2(self) -> None:
run_python_module(["pkg1.runmod2", "hello"])
out, err = self.stdouterr()
assert out == "pkg1.__init__: pkg1\nrunmod2: passed hello\n"
assert err == ""
- def test_runmod3(self):
+ def test_runmod3(self) -> None:
run_python_module(["pkg1.sub.runmod3", "hello"])
out, err = self.stdouterr()
assert out == "pkg1.__init__: pkg1\nrunmod3: passed hello\n"
assert err == ""
- def test_pkg1_main(self):
+ def test_pkg1_main(self) -> None:
run_python_module(["pkg1", "hello"])
out, err = self.stdouterr()
assert out == "pkg1.__init__: pkg1\npkg1.__main__: passed hello\n"
assert err == ""
- def test_pkg1_sub_main(self):
+ def test_pkg1_sub_main(self) -> None:
run_python_module(["pkg1.sub", "hello"])
out, err = self.stdouterr()
assert out == "pkg1.__init__: pkg1\npkg1.sub.__main__: passed hello\n"
assert err == ""
- def test_pkg1_init(self):
+ def test_pkg1_init(self) -> None:
run_python_module(["pkg1.__init__", "wut?"])
out, err = self.stdouterr()
assert out == "pkg1.__init__: pkg1\npkg1.__init__: __main__\n"
assert err == ""
- def test_no_such_module(self):
+ def test_no_such_module(self) -> None:
with pytest.raises(NoSource, match="No module named '?i_dont_exist'?"):
run_python_module(["i_dont_exist"])
with pytest.raises(NoSource, match="No module named '?i'?"):
@@ -314,6 +314,6 @@
with pytest.raises(NoSource, match="No module named '?i'?"):
run_python_module(["i.dont.exist"])
- def test_no_main(self):
+ def test_no_main(self) -> None:
with pytest.raises(NoSource):
run_python_module(["pkg2", "hi"])
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_filereporter.py python-coverage-7.2.7+dfsg1/tests/test_filereporter.py
--- python-coverage-6.5.0+dfsg1/tests/test_filereporter.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_filereporter.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,8 @@
"""Tests for FileReporters"""
+from __future__ import annotations
+
import sys
from coverage.plugin import FileReporter
@@ -20,7 +22,7 @@
run_in_temp_dir = False
- def test_filenames(self):
+ def test_filenames(self) -> None:
acu = PythonFileReporter("aa/afile.py")
bcu = PythonFileReporter("aa/bb/bfile.py")
ccu = PythonFileReporter("aa/bb/cc/cfile.py")
@@ -31,7 +33,7 @@
assert bcu.source() == "# bfile.py\n"
assert ccu.source() == "# cfile.py\n"
- def test_odd_filenames(self):
+ def test_odd_filenames(self) -> None:
acu = PythonFileReporter("aa/afile.odd.py")
bcu = PythonFileReporter("aa/bb/bfile.odd.py")
b2cu = PythonFileReporter("aa/bb.odd/bfile.py")
@@ -42,7 +44,7 @@
assert bcu.source() == "# bfile.odd.py\n"
assert b2cu.source() == "# bfile.py\n"
- def test_modules(self):
+ def test_modules(self) -> None:
import aa
import aa.bb
import aa.bb.cc
@@ -57,7 +59,7 @@
assert bcu.source() == "# bb\n"
assert ccu.source() == "" # yes, empty
- def test_module_files(self):
+ def test_module_files(self) -> None:
import aa.afile
import aa.bb.bfile
import aa.bb.cc.cfile
@@ -72,7 +74,7 @@
assert bcu.source() == "# bfile.py\n"
assert ccu.source() == "# cfile.py\n"
- def test_comparison(self):
+ def test_comparison(self) -> None:
acu = FileReporter("aa/afile.py")
acu2 = FileReporter("aa/afile.py")
zcu = FileReporter("aa/zfile.py")
@@ -83,7 +85,7 @@
assert acu < bcu and acu <= bcu and acu != bcu
assert bcu > acu and bcu >= acu and bcu != acu
- def test_zipfile(self):
+ def test_zipfile(self) -> None:
sys.path.append("tests/zip1.zip")
# Test that we can get files out of zipfiles, and read their source files.
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_files.py python-coverage-7.2.7+dfsg1/tests/test_files.py
--- python-coverage-6.5.0+dfsg1/tests/test_files.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_files.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,8 +3,14 @@
"""Tests for files.py"""
+from __future__ import annotations
+
+import itertools
import os
import os.path
+import re
+
+from typing import Any, Iterable, Iterator, List
from unittest import mock
import pytest
@@ -12,20 +18,23 @@
from coverage import env, files
from coverage.exceptions import ConfigError
from coverage.files import (
- FnmatchMatcher, ModuleMatcher, PathAliases, TreeMatcher, abs_file,
- actual_path, find_python_files, flat_rootname, fnmatches_to_regex,
+ GlobMatcher, ModuleMatcher, PathAliases, TreeMatcher, abs_file,
+ actual_path, find_python_files, flat_rootname, globs_to_regex,
)
+from coverage.types import Protocol
+
from tests.coveragetest import CoverageTest
+from tests.helpers import os_sep
class FilesTest(CoverageTest):
"""Tests of coverage.files."""
- def abs_path(self, p):
+ def abs_path(self, p: str) -> str:
"""Return the absolute path for `p`."""
return os.path.join(abs_file(os.getcwd()), os.path.normpath(p))
- def test_simple(self):
+ def test_simple(self) -> None:
self.make_file("hello.py")
files.set_relative_directory()
assert files.relative_filename("hello.py") == "hello.py"
@@ -33,7 +42,7 @@
assert a != "hello.py"
assert files.relative_filename(a) == "hello.py"
- def test_peer_directories(self):
+ def test_peer_directories(self) -> None:
self.make_file("sub/proj1/file1.py")
self.make_file("sub/proj2/file2.py")
a1 = self.abs_path("sub/proj1/file1.py")
@@ -44,7 +53,7 @@
assert files.relative_filename(a1) == "file1.py"
assert files.relative_filename(a2) == a2
- def test_filepath_contains_absolute_prefix_twice(self):
+ def test_filepath_contains_absolute_prefix_twice(self) -> None:
# https://github.com/nedbat/coveragepy/issues/194
# Build a path that has two pieces matching the absolute path prefix.
# Technically, this test doesn't do that on Windows, but drive
@@ -55,7 +64,7 @@
rel = os.path.join('sub', trick, 'file1.py')
assert files.relative_filename(abs_file(rel)) == rel
- def test_canonical_filename_ensure_cache_hit(self):
+ def test_canonical_filename_ensure_cache_hit(self) -> None:
self.make_file("sub/proj1/file1.py")
d = actual_path(self.abs_path("sub/proj1"))
os.chdir(d)
@@ -67,18 +76,36 @@
assert files.canonical_filename('sub/proj1/file1.py') == self.abs_path('file1.py')
@pytest.mark.parametrize(
- ["curdir", "sep"], [
+ "curdir, sep", [
("/", "/"),
("X:\\", "\\"),
]
)
- def test_relative_dir_for_root(self, curdir, sep):
+ def test_relative_dir_for_root(self, curdir: str, sep: str) -> None:
with mock.patch.object(files.os, 'curdir', new=curdir):
with mock.patch.object(files.os, 'sep', new=sep):
with mock.patch('coverage.files.os.path.normcase', return_value=curdir):
files.set_relative_directory()
assert files.relative_directory() == curdir
+ @pytest.mark.parametrize(
+ "to_make, to_check, answer", [
+ ("a/b/c/foo.py", "a/b/c/foo.py", True),
+ ("a/b/c/foo.py", "a/b/c/bar.py", False),
+ ("src/files.zip", "src/files.zip/foo.py", True),
+ ("src/files.whl", "src/files.whl/foo.py", True),
+ ("src/files.egg", "src/files.egg/foo.py", True),
+ ("src/files.pex", "src/files.pex/foo.py", True),
+ ("src/files.zip", "src/morefiles.zip/foo.py", False),
+ ("src/files.pex", "src/files.pex/zipfiles/files.zip/foo.py", True),
+ ]
+ )
+ def test_source_exists(self, to_make: str, to_check: str, answer: bool) -> None:
+ # source_exists won't look inside the zipfile, so it's fine to make
+ # an empty file with the zipfile name.
+ self.make_file(to_make, "")
+ assert files.source_exists(to_check) == answer
+
@pytest.mark.parametrize("original, flat", [
("abc.py", "abc_py"),
@@ -100,73 +127,179 @@
"d_e597dfacb73a23d5_my_program_py"
),
])
-def test_flat_rootname(original, flat):
+def test_flat_rootname(original: str, flat: str) -> None:
assert flat_rootname(original) == flat
+def globs_to_regex_params(
+ patterns: Iterable[str],
+ case_insensitive: bool = False,
+ partial: bool = False,
+ matches: Iterable[str] = (),
+ nomatches: Iterable[str] = (),
+) -> Iterator[Any]:
+ """Generate parameters for `test_globs_to_regex`.
+
+ `patterns`, `case_insensitive`, and `partial` are arguments for
+ `globs_to_regex`. `matches` is a list of strings that should match, and
+ `nomatches` is a list of strings that should not match.
+
+ Everything is yielded so that `test_globs_to_regex` can call
+ `globs_to_regex` once and check one result.
+
+ """
+ pat_id = "|".join(patterns)
+ for text in matches:
+ yield pytest.param(
+ patterns, case_insensitive, partial, text, True,
+ id=f"{pat_id}:ci{case_insensitive}:par{partial}:{text}:match",
+ )
+ for text in nomatches:
+ yield pytest.param(
+ patterns, case_insensitive, partial, text, False,
+ id=f"{pat_id}:ci{case_insensitive}:par{partial}:{text}:nomatch",
+ )
+
@pytest.mark.parametrize(
- "patterns, case_insensitive, partial," +
- "matches," +
- "nomatches",
-[
- (
- ["abc", "xyz"], False, False,
+ "patterns, case_insensitive, partial, text, result",
+ list(itertools.chain.from_iterable([
+ globs_to_regex_params(
["abc", "xyz"],
- ["ABC", "xYz", "abcx", "xabc", "axyz", "xyza"],
- ),
- (
- ["abc", "xyz"], True, False,
- ["abc", "xyz", "Abc", "XYZ", "AbC"],
- ["abcx", "xabc", "axyz", "xyza"],
- ),
- (
- ["abc/hi.py"], True, False,
- ["abc/hi.py", "ABC/hi.py", r"ABC\hi.py"],
- ["abc_hi.py", "abc/hi.pyc"],
- ),
- (
- [r"abc\hi.py"], True, False,
- [r"abc\hi.py", r"ABC\hi.py"],
- ["abc/hi.py", "ABC/hi.py", "abc_hi.py", "abc/hi.pyc"],
- ),
- (
- ["abc/*/hi.py"], True, False,
- ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"],
- ["abc/hi.py", "abc/hi.pyc"],
- ),
- (
- ["abc/[a-f]*/hi.py"], True, False,
- ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"],
- ["abc/zoo/hi.py", "abc/hi.py", "abc/hi.pyc"],
- ),
- (
- ["abc/"], True, True,
- ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"],
- ["abcd/foo.py", "xabc/hi.py"],
- ),
+ matches=["abc", "xyz", "sub/mod/abc"],
+ nomatches=[
+ "ABC", "xYz", "abcx", "xabc", "axyz", "xyza", "sub/mod/abcd", "sub/abc/more",
+ ],
+ ),
+ globs_to_regex_params(
+ ["abc", "xyz"], case_insensitive=True,
+ matches=["abc", "xyz", "Abc", "XYZ", "AbC"],
+ nomatches=["abcx", "xabc", "axyz", "xyza"],
+ ),
+ globs_to_regex_params(
+ ["a*c", "x*z"],
+ matches=["abc", "xyz", "xYz", "azc", "xaz", "axyzc"],
+ nomatches=["ABC", "abcx", "xabc", "axyz", "xyza", "a/c"],
+ ),
+ globs_to_regex_params(
+ ["a?c", "x?z"],
+ matches=["abc", "xyz", "xYz", "azc", "xaz"],
+ nomatches=["ABC", "abcx", "xabc", "axyz", "xyza", "a/c"],
+ ),
+ globs_to_regex_params(
+ ["a??d"],
+ matches=["abcd", "azcd", "a12d"],
+ nomatches=["ABCD", "abcx", "axyz", "abcde"],
+ ),
+ globs_to_regex_params(
+ ["abc/hi.py"], case_insensitive=True,
+ matches=["abc/hi.py", "ABC/hi.py", r"ABC\hi.py"],
+ nomatches=["abc_hi.py", "abc/hi.pyc"],
+ ),
+ globs_to_regex_params(
+ [r"abc\hi.py"], case_insensitive=True,
+ matches=[r"abc\hi.py", r"ABC\hi.py", "abc/hi.py", "ABC/hi.py"],
+ nomatches=["abc_hi.py", "abc/hi.pyc"],
+ ),
+ globs_to_regex_params(
+ ["abc/*/hi.py"], case_insensitive=True,
+ matches=["abc/foo/hi.py", r"ABC\foo/hi.py"],
+ nomatches=["abc/hi.py", "abc/hi.pyc", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"],
+ ),
+ globs_to_regex_params(
+ ["abc/**/hi.py"], case_insensitive=True,
+ matches=[
+ "abc/foo/hi.py", r"ABC\foo/hi.py", "abc/hi.py", "ABC/foo/bar/hi.py",
+ r"ABC\foo/bar/hi.py",
+ ],
+ nomatches=["abc/hi.pyc"],
+ ),
+ globs_to_regex_params(
+ ["abc/[a-f]*/hi.py"], case_insensitive=True,
+ matches=["abc/foo/hi.py", r"ABC\boo/hi.py"],
+ nomatches=[
+ "abc/zoo/hi.py", "abc/hi.py", "abc/hi.pyc", "abc/foo/bar/hi.py",
+ r"abc\foo/bar/hi.py",
+ ],
+ ),
+ globs_to_regex_params(
+ ["abc/[a-f]/hi.py"], case_insensitive=True,
+ matches=["abc/f/hi.py", r"ABC\b/hi.py"],
+ nomatches=[
+ "abc/foo/hi.py", "abc/zoo/hi.py", "abc/hi.py", "abc/hi.pyc", "abc/foo/bar/hi.py",
+ r"abc\foo/bar/hi.py",
+ ],
+ ),
+ globs_to_regex_params(
+ ["abc/"], case_insensitive=True, partial=True,
+ matches=["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"],
+ nomatches=["abcd/foo.py", "xabc/hi.py"],
+ ),
+ globs_to_regex_params(
+ ["*/foo"], case_insensitive=False, partial=True,
+ matches=["abc/foo/hi.py", "foo/hi.py"],
+ nomatches=["abc/xfoo/hi.py"],
+ ),
+ globs_to_regex_params(
+ ["**/foo"],
+ matches=["foo", "hello/foo", "hi/there/foo"],
+ nomatches=["foob", "hello/foob", "hello/Foo"],
+ ),
+ globs_to_regex_params(
+ ["a+b/foo*", "x{y}z/foo*"],
+ matches=["a+b/foo", "a+b/foobar", "x{y}z/foobar"],
+ nomatches=["aab/foo", "ab/foo", "xyz/foo"],
+ ),
+ ]))
+)
+def test_globs_to_regex(
+ patterns: Iterable[str],
+ case_insensitive: bool,
+ partial: bool,
+ text: str,
+ result: bool,
+) -> None:
+ regex = globs_to_regex(patterns, case_insensitive=case_insensitive, partial=partial)
+ assert bool(regex.match(text)) == result
+
+
+@pytest.mark.parametrize("pattern, bad_word", [
+ ("***/foo.py", "***"),
+ ("bar/***/foo.py", "***"),
+ ("*****/foo.py", "*****"),
+ ("Hello]there", "]"),
+ ("Hello[there", "["),
+ ("x/a**/b.py", "a**"),
+ ("x/abcd**/b.py", "abcd**"),
+ ("x/**a/b.py", "**a"),
+ ("x/**/**/b.py", "**/**"),
])
-def test_fnmatches_to_regex(patterns, case_insensitive, partial, matches, nomatches):
- regex = fnmatches_to_regex(patterns, case_insensitive=case_insensitive, partial=partial)
- for s in matches:
- assert regex.match(s)
- for s in nomatches:
- assert not regex.match(s)
+def test_invalid_globs(pattern: str, bad_word: str) -> None:
+ msg = f"File pattern can't include {bad_word!r}"
+ with pytest.raises(ConfigError, match=re.escape(msg)):
+ globs_to_regex([pattern])
+
+
+class TMatcher(Protocol):
+ """The shape all Matchers have."""
+
+ def match(self, s: str) -> bool:
+ """Does this string match?"""
class MatcherTest(CoverageTest):
"""Tests of file matchers."""
- def setUp(self):
+ def setUp(self) -> None:
super().setUp()
files.set_relative_directory()
- def assertMatches(self, matcher, filepath, matches):
+ def assertMatches(self, matcher: TMatcher, filepath: str, matches: bool) -> None:
"""The `matcher` should agree with `matches` about `filepath`."""
canonical = files.canonical_filename(filepath)
msg = f"File {filepath} should have matched as {matches}"
assert matches == matcher.match(canonical), msg
- def test_tree_matcher(self):
+ def test_tree_matcher(self) -> None:
case_folding = env.WINDOWS
matches_to_try = [
(self.make_file("sub/file1.py"), True),
@@ -188,7 +321,7 @@
for filepath, matches in matches_to_try:
self.assertMatches(tm, filepath, matches)
- def test_module_matcher(self):
+ def test_module_matcher(self) -> None:
matches_to_try = [
('test', True),
('trash', False),
@@ -211,7 +344,7 @@
for modulename, matches in matches_to_try:
assert mm.match(modulename) == matches, modulename
- def test_fnmatch_matcher(self):
+ def test_glob_matcher(self) -> None:
matches_to_try = [
(self.make_file("sub/file1.py"), True),
(self.make_file("sub/file2.c"), False),
@@ -219,30 +352,32 @@
(self.make_file("sub3/file4.py"), True),
(self.make_file("sub3/file5.c"), False),
]
- fnm = FnmatchMatcher(["*.py", "*/sub2/*"])
+ fnm = GlobMatcher(["*.py", "*/sub2/*"])
assert fnm.info() == ["*.py", "*/sub2/*"]
for filepath, matches in matches_to_try:
self.assertMatches(fnm, filepath, matches)
- def test_fnmatch_matcher_overload(self):
- fnm = FnmatchMatcher(["*x%03d*.txt" % i for i in range(500)])
+ def test_glob_matcher_overload(self) -> None:
+ fnm = GlobMatcher(["*x%03d*.txt" % i for i in range(500)])
self.assertMatches(fnm, "x007foo.txt", True)
self.assertMatches(fnm, "x123foo.txt", True)
self.assertMatches(fnm, "x798bar.txt", False)
+ self.assertMatches(fnm, "x499.txt", True)
+ self.assertMatches(fnm, "x500.txt", False)
- def test_fnmatch_windows_paths(self):
+ def test_glob_windows_paths(self) -> None:
# We should be able to match Windows paths even if we are running on
# a non-Windows OS.
- fnm = FnmatchMatcher(["*/foo.py"])
+ fnm = GlobMatcher(["*/foo.py"])
self.assertMatches(fnm, r"dir\foo.py", True)
- fnm = FnmatchMatcher([r"*\foo.py"])
+ fnm = GlobMatcher([r"*\foo.py"])
self.assertMatches(fnm, r"dir\foo.py", True)
@pytest.fixture(params=[False, True], name="rel_yn")
-def relative_setting(request):
+def relative_setting(request: pytest.FixtureRequest) -> bool:
"""Parameterized fixture to choose whether PathAliases is relative or not."""
- return request.param
+ return request.param # type: ignore[no-any-return]
class PathAliasesTest(CoverageTest):
@@ -250,59 +385,83 @@
run_in_temp_dir = False
- def assert_mapped(self, aliases, inp, out, relative=False):
+ def assert_mapped(self, aliases: PathAliases, inp: str, out: str) -> None:
"""Assert that `inp` mapped through `aliases` produces `out`.
- `out` is canonicalized first, since aliases produce canonicalized
- paths by default.
+ If the aliases are not relative, then `out` is canonicalized first,
+ since aliases produce canonicalized paths by default.
"""
- mapped = aliases.map(inp)
- expected = files.canonical_filename(out) if not relative else out
+ mapped = aliases.map(inp, exists=lambda p: True)
+ if aliases.relative:
+ expected = out
+ else:
+ expected = files.canonical_filename(out)
assert mapped == expected
- def assert_unchanged(self, aliases, inp):
+ def assert_unchanged(self, aliases: PathAliases, inp: str, exists: bool = True) -> None:
"""Assert that `inp` mapped through `aliases` is unchanged."""
- assert aliases.map(inp) == inp
+ assert aliases.map(inp, exists=lambda p: exists) == inp
- def test_noop(self, rel_yn):
+ def test_noop(self, rel_yn: bool) -> None:
aliases = PathAliases(relative=rel_yn)
self.assert_unchanged(aliases, '/ned/home/a.py')
- def test_nomatch(self, rel_yn):
+ def test_nomatch(self, rel_yn: bool) -> None:
aliases = PathAliases(relative=rel_yn)
aliases.add('/home/*/src', './mysrc')
self.assert_unchanged(aliases, '/home/foo/a.py')
- def test_wildcard(self, rel_yn):
+ def test_wildcard(self, rel_yn: bool) -> None:
aliases = PathAliases(relative=rel_yn)
aliases.add('/ned/home/*/src', './mysrc')
- self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py', relative=rel_yn)
+ self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py')
aliases = PathAliases(relative=rel_yn)
aliases.add('/ned/home/*/src/', './mysrc')
- self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py', relative=rel_yn)
+ self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py')
- def test_no_accidental_match(self, rel_yn):
+ def test_no_accidental_match(self, rel_yn: bool) -> None:
aliases = PathAliases(relative=rel_yn)
aliases.add('/home/*/src', './mysrc')
self.assert_unchanged(aliases, '/home/foo/srcetc')
- def test_multiple_patterns(self, rel_yn):
+ def test_no_map_if_not_exist(self, rel_yn: bool) -> None:
+ aliases = PathAliases(relative=rel_yn)
+ aliases.add('/ned/home/*/src', './mysrc')
+ self.assert_unchanged(aliases, '/ned/home/foo/src/a.py', exists=False)
+ self.assert_unchanged(aliases, 'foo/src/a.py', exists=False)
+
+ def test_no_dotslash(self, rel_yn: bool) -> None:
+ # The result shouldn't start with "./" if the map result didn't.
+ aliases = PathAliases(relative=rel_yn)
+ aliases.add('*/project', '.')
+ self.assert_mapped(aliases, '/ned/home/project/src/a.py', os_sep('src/a.py'))
+
+ def test_relative_pattern(self) -> None:
+ aliases = PathAliases(relative=True)
+ aliases.add(".tox/*/site-packages", "src")
+ self.assert_mapped(
+ aliases,
+ ".tox/py314/site-packages/proj/a.py",
+ os_sep("src/proj/a.py"),
+ )
+
+ def test_multiple_patterns(self, rel_yn: bool) -> None:
# also test the debugfn...
- msgs = []
+ msgs: List[str] = []
aliases = PathAliases(debugfn=msgs.append, relative=rel_yn)
aliases.add('/home/*/src', './mysrc')
aliases.add('/lib/*/libsrc', './mylib')
- self.assert_mapped(aliases, '/home/foo/src/a.py', './mysrc/a.py', relative=rel_yn)
- self.assert_mapped(aliases, '/lib/foo/libsrc/a.py', './mylib/a.py', relative=rel_yn)
+ self.assert_mapped(aliases, '/home/foo/src/a.py', './mysrc/a.py')
+ self.assert_mapped(aliases, '/lib/foo/libsrc/a.py', './mylib/a.py')
if rel_yn:
assert msgs == [
"Aliases (relative=True):",
" Rule: '/home/*/src' -> './mysrc/' using regex " +
- "'(?:(?s:[\\\\\\\\/]home[\\\\\\\\/].*[\\\\\\\\/]src[\\\\\\\\/]))'",
+ "'[/\\\\\\\\]home[/\\\\\\\\][^/\\\\\\\\]*[/\\\\\\\\]src[/\\\\\\\\]'",
" Rule: '/lib/*/libsrc' -> './mylib/' using regex " +
- "'(?:(?s:[\\\\\\\\/]lib[\\\\\\\\/].*[\\\\\\\\/]libsrc[\\\\\\\\/]))'",
+ "'[/\\\\\\\\]lib[/\\\\\\\\][^/\\\\\\\\]*[/\\\\\\\\]libsrc[/\\\\\\\\]'",
"Matched path '/home/foo/src/a.py' to rule '/home/*/src' -> './mysrc/', " +
"producing './mysrc/a.py'",
"Matched path '/lib/foo/libsrc/a.py' to rule '/lib/*/libsrc' -> './mylib/', " +
@@ -312,9 +471,9 @@
assert msgs == [
"Aliases (relative=False):",
" Rule: '/home/*/src' -> './mysrc/' using regex " +
- "'(?:(?s:[\\\\\\\\/]home[\\\\\\\\/].*[\\\\\\\\/]src[\\\\\\\\/]))'",
+ "'[/\\\\\\\\]home[/\\\\\\\\][^/\\\\\\\\]*[/\\\\\\\\]src[/\\\\\\\\]'",
" Rule: '/lib/*/libsrc' -> './mylib/' using regex " +
- "'(?:(?s:[\\\\\\\\/]lib[\\\\\\\\/].*[\\\\\\\\/]libsrc[\\\\\\\\/]))'",
+ "'[/\\\\\\\\]lib[/\\\\\\\\][^/\\\\\\\\]*[/\\\\\\\\]libsrc[/\\\\\\\\]'",
"Matched path '/home/foo/src/a.py' to rule '/home/*/src' -> './mysrc/', " +
f"producing {files.canonical_filename('./mysrc/a.py')!r}",
"Matched path '/lib/foo/libsrc/a.py' to rule '/lib/*/libsrc' -> './mylib/', " +
@@ -326,24 +485,24 @@
"/ned/home/*/",
"/ned/home/*/*/",
])
- def test_cant_have_wildcard_at_end(self, badpat):
+ def test_cant_have_wildcard_at_end(self, badpat: str) -> None:
aliases = PathAliases()
msg = "Pattern must not end with wildcards."
with pytest.raises(ConfigError, match=msg):
aliases.add(badpat, "fooey")
- def test_no_accidental_munging(self):
+ def test_no_accidental_munging(self) -> None:
aliases = PathAliases()
aliases.add(r'c:\Zoo\boo', 'src/')
aliases.add('/home/ned$', 'src/')
self.assert_mapped(aliases, r'c:\Zoo\boo\foo.py', 'src/foo.py')
self.assert_mapped(aliases, r'/home/ned$/foo.py', 'src/foo.py')
- def test_paths_are_os_corrected(self, rel_yn):
+ def test_paths_are_os_corrected(self, rel_yn: bool) -> None:
aliases = PathAliases(relative=rel_yn)
aliases.add('/home/ned/*/src', './mysrc')
aliases.add(r'c:\ned\src', './mysrc')
- self.assert_mapped(aliases, r'C:\Ned\src\sub\a.py', './mysrc/sub/a.py', relative=rel_yn)
+ self.assert_mapped(aliases, r'C:\Ned\src\sub\a.py', './mysrc/sub/a.py')
aliases = PathAliases(relative=rel_yn)
aliases.add('/home/ned/*/src', r'.\mysrc')
@@ -352,83 +511,117 @@
aliases,
r'/home/ned/foo/src/sub/a.py',
r'.\mysrc\sub\a.py',
- relative=rel_yn,
)
- def test_windows_on_linux(self, rel_yn):
- # https://github.com/nedbat/coveragepy/issues/618
- lin = "*/project/module/"
- win = "*\\project\\module\\"
+ # Try the paths in both orders.
+ lin = "*/project/module/"
+ win = "*\\project\\module\\"
+ lin_win_paths = [[lin, win], [win, lin]]
- # Try the paths in both orders.
- for paths in [[lin, win], [win, lin]]:
- aliases = PathAliases(relative=rel_yn)
- for path in paths:
- aliases.add(path, "project/module")
- self.assert_mapped(
- aliases,
- "C:\\a\\path\\somewhere\\coveragepy_test\\project\\module\\tests\\file.py",
- "project/module/tests/file.py",
- relative=rel_yn,
- )
+ @pytest.mark.parametrize("paths", lin_win_paths)
+ def test_windows_on_linux(self, paths: Iterable[str], rel_yn: bool) -> None:
+ # https://github.com/nedbat/coveragepy/issues/618
+ aliases = PathAliases(relative=rel_yn)
+ for path in paths:
+ aliases.add(path, "project/module")
+ self.assert_mapped(
+ aliases,
+ "C:\\a\\path\\somewhere\\coveragepy_test\\project\\module\\tests\\file.py",
+ "project/module/tests/file.py",
+ )
- def test_linux_on_windows(self, rel_yn):
+ @pytest.mark.parametrize("paths", lin_win_paths)
+ def test_linux_on_windows(self, paths: Iterable[str], rel_yn: bool) -> None:
# https://github.com/nedbat/coveragepy/issues/618
- lin = "*/project/module/"
- win = "*\\project\\module\\"
+ aliases = PathAliases(relative=rel_yn)
+ for path in paths:
+ aliases.add(path, "project\\module")
+ self.assert_mapped(
+ aliases,
+ "C:/a/path/somewhere/coveragepy_test/project/module/tests/file.py",
+ "project\\module\\tests\\file.py",
+ )
- # Try the paths in both orders.
- for paths in [[lin, win], [win, lin]]:
- aliases = PathAliases(relative=rel_yn)
- for path in paths:
- aliases.add(path, "project\\module")
- self.assert_mapped(
- aliases,
- "C:/a/path/somewhere/coveragepy_test/project/module/tests/file.py",
- "project\\module\\tests\\file.py",
- relative=rel_yn,
- )
+ @pytest.mark.parametrize("paths", lin_win_paths)
+ def test_relative_windows_on_linux(self, paths: Iterable[str]) -> None:
+ # https://github.com/nedbat/coveragepy/issues/991
+ aliases = PathAliases(relative=True)
+ for path in paths:
+ aliases.add(path, "project/module")
+ self.assert_mapped(
+ aliases,
+ r"project\module\tests\file.py",
+ r"project/module/tests/file.py",
+ )
- def test_multiple_wildcard(self, rel_yn):
+ @pytest.mark.parametrize("paths", lin_win_paths)
+ def test_relative_linux_on_windows(self, paths: Iterable[str]) -> None:
+ # https://github.com/nedbat/coveragepy/issues/991
+ aliases = PathAliases(relative=True)
+ for path in paths:
+ aliases.add(path, r"project\module")
+ self.assert_mapped(
+ aliases,
+ r"project/module/tests/file.py",
+ r"project\module\tests\file.py",
+ )
+
+ @pytest.mark.skipif(env.WINDOWS, reason="This test assumes Unix file system")
+ def test_implicit_relative_windows_on_linux(self) -> None:
+ # https://github.com/nedbat/coveragepy/issues/991
+ aliases = PathAliases(relative=True)
+ self.assert_mapped(
+ aliases,
+ r"project\module\tests\file.py",
+ r"project/module/tests/file.py",
+ )
+
+ @pytest.mark.skipif(not env.WINDOWS, reason="This test assumes Windows file system")
+ def test_implicit_relative_linux_on_windows(self) -> None:
+ # https://github.com/nedbat/coveragepy/issues/991
+ aliases = PathAliases(relative=True)
+ self.assert_mapped(
+ aliases,
+ r"project/module/tests/file.py",
+ r"project\module\tests\file.py",
+ )
+
+ def test_multiple_wildcard(self, rel_yn: bool) -> None:
aliases = PathAliases(relative=rel_yn)
aliases.add('/home/jenkins/*/a/*/b/*/django', './django')
self.assert_mapped(
aliases,
'/home/jenkins/xx/a/yy/b/zz/django/foo/bar.py',
'./django/foo/bar.py',
- relative=rel_yn,
)
- def test_windows_root_paths(self, rel_yn):
+ def test_windows_root_paths(self, rel_yn: bool) -> None:
aliases = PathAliases(relative=rel_yn)
aliases.add('X:\\', '/tmp/src')
self.assert_mapped(
aliases,
"X:\\a\\file.py",
"/tmp/src/a/file.py",
- relative=rel_yn,
)
self.assert_mapped(
aliases,
"X:\\file.py",
"/tmp/src/file.py",
- relative=rel_yn,
)
- def test_leading_wildcard(self, rel_yn):
+ def test_leading_wildcard(self, rel_yn: bool) -> None:
aliases = PathAliases(relative=rel_yn)
aliases.add('*/d1', './mysrc1')
aliases.add('*/d2', './mysrc2')
- self.assert_mapped(aliases, '/foo/bar/d1/x.py', './mysrc1/x.py', relative=rel_yn)
- self.assert_mapped(aliases, '/foo/bar/d2/y.py', './mysrc2/y.py', relative=rel_yn)
+ self.assert_mapped(aliases, '/foo/bar/d1/x.py', './mysrc1/x.py')
+ self.assert_mapped(aliases, '/foo/bar/d2/y.py', './mysrc2/y.py')
- # The root test case was added for the manylinux Docker images,
- # and I'm not sure how it should work on Windows, so skip it.
- cases = [".", "..", "../other"]
- if not env.WINDOWS:
- cases += ["/"]
- @pytest.mark.parametrize("dirname", cases)
- def test_dot(self, dirname):
+ @pytest.mark.parametrize("dirname", [".", "..", "../other", "/"])
+ def test_dot(self, dirname: str) -> None:
+ if env.WINDOWS and dirname == "/":
+ # The root test case was added for the manylinux Docker images,
+ # and I'm not sure how it should work on Windows, so skip it.
+ pytest.skip("Don't know how to handle root on Windows")
aliases = PathAliases()
aliases.add(dirname, '/the/source')
the_file = os.path.join(dirname, 'a.py')
@@ -439,10 +632,23 @@
self.assert_mapped(aliases, the_file, '/the/source/a.py')
+class PathAliasesRealFilesTest(CoverageTest):
+ """Tests for coverage/files.py:PathAliases using real files."""
+
+ def test_aliasing_zip_files(self) -> None:
+ self.make_file("src/zipfiles/code.zip", "fake zip, doesn't matter")
+ aliases = PathAliases()
+ aliases.add("*/d1", "./src")
+ aliases.add("*/d2", "./src")
+
+ expected = files.canonical_filename("src/zipfiles/code.zip/p1.py")
+ assert aliases.map("tox/d1/zipfiles/code.zip/p1.py") == expected
+
+
class FindPythonFilesTest(CoverageTest):
"""Tests of `find_python_files`."""
- def test_find_python_files(self):
+ def test_find_python_files(self) -> None:
self.make_file("sub/a.py")
self.make_file("sub/b.py")
self.make_file("sub/x.c") # nope: not .py
@@ -451,10 +657,27 @@
self.make_file("sub/ssub/~s.py") # nope: editor effluvia
self.make_file("sub/lab/exp.py") # nope: no __init__.py
self.make_file("sub/windows.pyw")
- py_files = set(find_python_files("sub"))
+ py_files = set(find_python_files("sub", include_namespace_packages=False))
+ self.assert_same_files(py_files, [
+ "sub/a.py", "sub/b.py",
+ "sub/ssub/__init__.py", "sub/ssub/s.py",
+ "sub/windows.pyw",
+ ])
+
+ def test_find_python_files_include_namespace_packages(self) -> None:
+ self.make_file("sub/a.py")
+ self.make_file("sub/b.py")
+ self.make_file("sub/x.c") # nope: not .py
+ self.make_file("sub/ssub/__init__.py")
+ self.make_file("sub/ssub/s.py")
+ self.make_file("sub/ssub/~s.py") # nope: editor effluvia
+ self.make_file("sub/lab/exp.py")
+ self.make_file("sub/windows.pyw")
+ py_files = set(find_python_files("sub", include_namespace_packages=True))
self.assert_same_files(py_files, [
"sub/a.py", "sub/b.py",
"sub/ssub/__init__.py", "sub/ssub/s.py",
+ "sub/lab/exp.py",
"sub/windows.pyw",
])
@@ -465,5 +688,5 @@
run_in_temp_dir = False
- def test_actual_path(self):
+ def test_actual_path(self) -> None:
assert actual_path(r'c:\Windows') == actual_path(r'C:\wINDOWS')
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_goldtest.py python-coverage-7.2.7+dfsg1/tests/test_goldtest.py
--- python-coverage-6.5.0+dfsg1/tests/test_goldtest.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_goldtest.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,8 @@
"""Tests of the helpers in goldtest.py"""
+from __future__ import annotations
+
import os.path
import re
@@ -11,7 +13,7 @@
from tests.coveragetest import CoverageTest, TESTS_DIR
from tests.goldtest import compare, gold_path
from tests.goldtest import contains, contains_any, contains_rx, doesnt_contain
-from tests.helpers import re_line, remove_tree
+from tests.helpers import os_sep, re_line, remove_tree
GOOD_GETTY = """\
Four score and seven years ago our fathers brought forth upon this continent, a
@@ -33,7 +35,7 @@
(r'G\w+', 'Gxxx'),
]
-def path_regex(path):
+def path_regex(path: str) -> str:
"""Convert a file path into a regex that will match that path on any OS."""
return re.sub(r"[/\\]", r"[/\\\\]", path.replace(".", "[.]"))
@@ -48,16 +50,16 @@
class CompareTest(CoverageTest):
"""Tests of goldtest.py:compare()"""
- def setUp(self):
+ def setUp(self) -> None:
super().setUp()
self.addCleanup(remove_tree, ACTUAL_DIR)
- def test_good(self):
+ def test_good(self) -> None:
self.make_file("out/gettysburg.txt", GOOD_GETTY)
compare(gold_path("testing/getty"), "out", scrubs=SCRUBS)
self.assert_doesnt_exist(ACTUAL_GETTY_FILE)
- def test_bad(self):
+ def test_bad(self) -> None:
self.make_file("out/gettysburg.txt", BAD_GETTY)
# compare() raises an assertion.
@@ -71,6 +73,10 @@
assert "+ Five score" in stdout
assert re_line(rf"^:::: diff '.*{GOLD_PATH_RX}' and '{OUT_PATH_RX}'", stdout)
assert re_line(rf"^:::: end diff '.*{GOLD_PATH_RX}' and '{OUT_PATH_RX}'", stdout)
+ assert (
+ os_sep(f"Saved actual output to '{ACTUAL_GETTY_FILE}': see tests/gold/README.rst")
+ in os_sep(stdout)
+ )
assert " D/D/D, Gxxx, Pennsylvania" in stdout
# The actual file was saved.
@@ -78,7 +84,7 @@
saved = f.read()
assert saved == BAD_GETTY
- def test_good_needs_scrubs(self):
+ def test_good_needs_scrubs(self) -> None:
# Comparing the "good" result without scrubbing the variable parts will fail.
self.make_file("out/gettysburg.txt", GOOD_GETTY)
@@ -91,7 +97,7 @@
assert "- 11/19/1863, Gettysburg, Pennsylvania" in stdout
assert "+ 11/19/9999, Gettysburg, Pennsylvania" in stdout
- def test_actual_extra(self):
+ def test_actual_extra(self) -> None:
self.make_file("out/gettysburg.txt", GOOD_GETTY)
self.make_file("out/another.more", "hi")
@@ -107,7 +113,7 @@
# But only the files matching the file_pattern are considered.
compare(gold_path("testing/getty"), "out", file_pattern="*.txt", scrubs=SCRUBS)
- def test_xml_good(self):
+ def test_xml_good(self) -> None:
self.make_file("out/output.xml", """\
@@ -118,7 +124,7 @@
""")
compare(gold_path("testing/xml"), "out", scrubs=SCRUBS)
- def test_xml_bad(self):
+ def test_xml_bad(self) -> None:
self.make_file("out/output.xml", """\
@@ -147,25 +153,25 @@
run_in_temp_dir = False
- def test_contains(self):
+ def test_contains(self) -> None:
contains(GOLD_GETTY_FILE, "Four", "fathers", "dedicated")
msg = rf"Missing content in {GOLD_GETTY_FILE_RX}: 'xyzzy'"
with pytest.raises(AssertionError, match=msg):
contains(GOLD_GETTY_FILE, "Four", "fathers", "xyzzy", "dedicated")
- def test_contains_rx(self):
+ def test_contains_rx(self) -> None:
contains_rx(GOLD_GETTY_FILE, r"Fo.r", r"f[abc]thers", "dedi[cdef]ated")
msg = rf"Missing regex in {GOLD_GETTY_FILE_RX}: r'm\[opq\]thers'"
with pytest.raises(AssertionError, match=msg):
contains_rx(GOLD_GETTY_FILE, r"Fo.r", r"m[opq]thers")
- def test_contains_any(self):
+ def test_contains_any(self) -> None:
contains_any(GOLD_GETTY_FILE, "Five", "Four", "Three")
msg = rf"Missing content in {GOLD_GETTY_FILE_RX}: 'One' \[1 of 3\]"
with pytest.raises(AssertionError, match=msg):
contains_any(GOLD_GETTY_FILE, "One", "Two", "Three")
- def test_doesnt_contain(self):
+ def test_doesnt_contain(self) -> None:
doesnt_contain(GOLD_GETTY_FILE, "One", "Two", "Three")
msg = rf"Forbidden content in {GOLD_GETTY_FILE_RX}: 'Four'"
with pytest.raises(AssertionError, match=msg):
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_html.py python-coverage-7.2.7+dfsg1/tests/test_html.py
--- python-coverage-6.5.0+dfsg1/tests/test_html.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_html.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,8 @@
"""Tests that HTML generation is awesome."""
+from __future__ import annotations
+
import collections
import datetime
import glob
@@ -13,14 +15,17 @@
import sys
from unittest import mock
+from typing import Any, Dict, IO, List, Optional, Set, Tuple
+
import pytest
import coverage
-from coverage import env
+from coverage import env, Coverage
from coverage.exceptions import NoDataError, NotPython, NoSource
from coverage.files import abs_file, flat_rootname
import coverage.html
-from coverage.report import get_analysis_to_report
+from coverage.report_core import get_analysis_to_report
+from coverage.types import TLineNo, TMorf
from tests.coveragetest import CoverageTest, TESTS_DIR
from tests.goldtest import gold_path
@@ -31,7 +36,7 @@
class HtmlTestHelpers(CoverageTest):
"""Methods that help with HTML tests."""
- def create_initial_files(self):
+ def create_initial_files(self) -> None:
"""Create the source files we need to run these tests."""
self.make_file("main_file.py", """\
import helper1, helper2
@@ -48,7 +53,11 @@
print("x is %d" % x)
""")
- def run_coverage(self, covargs=None, htmlargs=None):
+ def run_coverage(
+ self,
+ covargs: Optional[Dict[str, Any]] = None,
+ htmlargs: Optional[Dict[str, Any]] = None,
+ ) -> float:
"""Run coverage.py on main_file.py, and create an HTML report."""
self.clean_local_file_imports()
cov = coverage.Coverage(**(covargs or {}))
@@ -57,17 +66,17 @@
self.assert_valid_hrefs()
return ret
- def get_html_report_content(self, module):
+ def get_html_report_content(self, module: str) -> str:
"""Return the content of the HTML report for `module`."""
filename = flat_rootname(module) + ".html"
filename = os.path.join("htmlcov", filename)
with open(filename) as f:
return f.read()
- def get_html_index_content(self):
+ def get_html_index_content(self) -> str:
"""Return the content of index.html.
- Timestamps are replaced with a placeholder so that clocks don't matter.
+ Time stamps are replaced with a placeholder so that clocks don't matter.
"""
with open("htmlcov/index.html") as f:
@@ -84,21 +93,21 @@
)
return index
- def assert_correct_timestamp(self, html):
- """Extract the timestamp from `html`, and assert it is recent."""
+ def assert_correct_timestamp(self, html: str) -> None:
+ """Extract the time stamp from `html`, and assert it is recent."""
timestamp_pat = r"created at (\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})"
m = re.search(timestamp_pat, html)
- assert m, "Didn't find a timestamp!"
- timestamp = datetime.datetime(*map(int, m.groups()))
- # The timestamp only records the minute, so the delta could be from
+ assert m, "Didn't find a time stamp!"
+ timestamp = datetime.datetime(*[int(v) for v in m.groups()]) # type: ignore[arg-type]
+ # The time stamp only records the minute, so the delta could be from
# 12:00 to 12:01:59, or two minutes.
self.assert_recent_datetime(
timestamp,
seconds=120,
- msg=f"Timestamp is wrong: {timestamp}",
+ msg=f"Time stamp is wrong: {timestamp}",
)
- def assert_valid_hrefs(self):
+ def assert_valid_hrefs(self) -> None:
"""Assert that the hrefs in htmlcov/*.html to see the references are valid.
Doesn't check external links (those with a protocol).
@@ -124,10 +133,10 @@
class FileWriteTracker:
"""A fake object to track how `open` is used to write files."""
- def __init__(self, written):
+ def __init__(self, written: Set[str]) -> None:
self.written = written
- def open(self, filename, mode="r"):
+ def open(self, filename: str, mode: str = "r") -> IO[str]:
"""Be just like `open`, but write written file names to `self.written`."""
if mode.startswith("w"):
self.written.add(filename.replace('\\', '/'))
@@ -137,7 +146,7 @@
class HtmlDeltaTest(HtmlTestHelpers, CoverageTest):
"""Tests of the HTML delta speed-ups."""
- def setUp(self):
+ def setUp(self) -> None:
super().setUp()
# At least one of our tests monkey-patches the version of coverage.py,
@@ -145,9 +154,13 @@
self.real_coverage_version = coverage.__version__
self.addCleanup(setattr, coverage, "__version__", self.real_coverage_version)
- self.files_written = None
+ self.files_written: Set[str]
- def run_coverage(self, covargs=None, htmlargs=None):
+ def run_coverage(
+ self,
+ covargs: Optional[Dict[str, Any]] = None,
+ htmlargs: Optional[Dict[str, Any]] = None,
+ ) -> float:
"""Run coverage in-process for the delta tests.
For the delta tests, we always want `source=.` and we want to track
@@ -162,7 +175,7 @@
with mock.patch("coverage.html.open", mock_open):
return super().run_coverage(covargs=covargs, htmlargs=htmlargs)
- def assert_htmlcov_files_exist(self):
+ def assert_htmlcov_files_exist(self) -> None:
"""Assert that all the expected htmlcov files exist."""
self.assert_exists("htmlcov/index.html")
self.assert_exists("htmlcov/main_file_py.html")
@@ -172,13 +185,13 @@
self.assert_exists("htmlcov/coverage_html.js")
self.assert_exists("htmlcov/.gitignore")
- def test_html_created(self):
+ def test_html_created(self) -> None:
# Test basic HTML generation: files should be created.
self.create_initial_files()
self.run_coverage()
self.assert_htmlcov_files_exist()
- def test_html_delta_from_source_change(self):
+ def test_html_delta_from_source_change(self) -> None:
# HTML generation can create only the files that have changed.
# In this case, helper1 changes because its source is different.
self.create_initial_files()
@@ -205,7 +218,7 @@
index2 = self.get_html_index_content()
assert index1 == index2
- def test_html_delta_from_coverage_change(self):
+ def test_html_delta_from_coverage_change(self) -> None:
# HTML generation can create only the files that have changed.
# In this case, helper1 changes because its coverage is different.
self.create_initial_files()
@@ -228,7 +241,7 @@
assert "htmlcov/helper2_py.html" not in self.files_written
assert "htmlcov/main_file_py.html" in self.files_written
- def test_html_delta_from_settings_change(self):
+ def test_html_delta_from_settings_change(self) -> None:
# HTML generation can create only the files that have changed.
# In this case, everything changes because the coverage.py settings
# have changed.
@@ -248,7 +261,7 @@
index2 = self.get_html_index_content()
assert index1 == index2
- def test_html_delta_from_coverage_version_change(self):
+ def test_html_delta_from_coverage_version_change(self) -> None:
# HTML generation can create only the files that have changed.
# In this case, everything changes because the coverage.py version has
# changed.
@@ -272,7 +285,7 @@
fixed_index2 = index2.replace("XYZZY", self.real_coverage_version)
assert index1 == fixed_index2
- def test_file_becomes_100(self):
+ def test_file_becomes_100(self) -> None:
self.create_initial_files()
self.run_coverage()
@@ -289,7 +302,7 @@
# The 100% file, skipped, shouldn't be here.
self.assert_doesnt_exist("htmlcov/helper1_py.html")
- def test_status_format_change(self):
+ def test_status_format_change(self) -> None:
self.create_initial_files()
self.run_coverage()
@@ -310,14 +323,14 @@
assert "htmlcov/helper2_py.html" in self.files_written
assert "htmlcov/main_file_py.html" in self.files_written
- def test_dont_overwrite_gitignore(self):
+ def test_dont_overwrite_gitignore(self) -> None:
self.create_initial_files()
self.make_file("htmlcov/.gitignore", "# ignore nothing")
self.run_coverage()
with open("htmlcov/.gitignore") as fgi:
assert fgi.read() == "# ignore nothing"
- def test_dont_write_gitignore_into_existing_directory(self):
+ def test_dont_write_gitignore_into_existing_directory(self) -> None:
self.create_initial_files()
self.make_file("htmlcov/README", "My files: don't touch!")
self.run_coverage()
@@ -328,14 +341,14 @@
class HtmlTitleTest(HtmlTestHelpers, CoverageTest):
"""Tests of the HTML title support."""
- def test_default_title(self):
+ def test_default_title(self) -> None:
self.create_initial_files()
self.run_coverage()
index = self.get_html_index_content()
assert "Coverage report " in index
assert "Coverage report:" in index
- def test_title_set_in_config_file(self):
+ def test_title_set_in_config_file(self) -> None:
self.create_initial_files()
self.make_file(".coveragerc", "[html]\ntitle = Metrics & stuff!\n")
self.run_coverage()
@@ -343,7 +356,7 @@
assert "Metrics & stuff! " in index
assert "Metrics & stuff!:" in index
- def test_non_ascii_title_set_in_config_file(self):
+ def test_non_ascii_title_set_in_config_file(self) -> None:
self.create_initial_files()
self.make_file(".coveragerc", "[html]\ntitle = «ταБЬℓσ» numbers")
self.run_coverage()
@@ -351,7 +364,7 @@
assert "«ταБЬℓσ» numbers" in index
assert "«ταБЬℓσ» numbers" in index
- def test_title_set_in_args(self):
+ def test_title_set_in_args(self) -> None:
self.create_initial_files()
self.make_file(".coveragerc", "[html]\ntitle = Good title\n")
self.run_coverage(htmlargs=dict(title="«ταБЬℓσ» & stüff!"))
@@ -367,7 +380,7 @@
class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest):
"""Test the behavior when measuring unparsable files."""
- def test_dotpy_not_python(self):
+ def test_dotpy_not_python(self) -> None:
self.make_file("main.py", "import innocuous")
self.make_file("innocuous.py", "a = 1")
cov = coverage.Coverage()
@@ -377,7 +390,7 @@
with pytest.raises(NotPython, match=msg):
cov.html_report()
- def test_dotpy_not_python_ignored(self):
+ def test_dotpy_not_python_ignored(self) -> None:
self.make_file("main.py", "import innocuous")
self.make_file("innocuous.py", "a = 2")
cov = coverage.Coverage()
@@ -394,7 +407,7 @@
# This would be better as a glob, if the HTML layout changes:
self.assert_doesnt_exist("htmlcov/innocuous.html")
- def test_dothtml_not_python(self):
+ def test_dothtml_not_python(self) -> None:
# Run an "HTML" file
self.make_file("innocuous.html", "a = 3")
self.make_data_file(lines={abs_file("innocuous.html"): [1]})
@@ -405,7 +418,7 @@
with pytest.raises(NoDataError, match="No data to report."):
cov.html_report()
- def test_execed_liar_ignored(self):
+ def test_execed_liar_ignored(self) -> None:
# Jinja2 sets __file__ to be a non-Python file, and then execs code.
# If that file contains non-Python code, a TokenError shouldn't
# have been raised when writing the HTML report.
@@ -417,7 +430,7 @@
cov.html_report()
self.assert_exists("htmlcov/index.html")
- def test_execed_liar_ignored_indentation_error(self):
+ def test_execed_liar_ignored_indentation_error(self) -> None:
# Jinja2 sets __file__ to be a non-Python file, and then execs code.
# If that file contains untokenizable code, we shouldn't get an
# exception.
@@ -430,7 +443,7 @@
cov.html_report()
self.assert_exists("htmlcov/index.html")
- def test_decode_error(self):
+ def test_decode_error(self) -> None:
# https://github.com/nedbat/coveragepy/issues/351
# imp.load_module won't load a file with an undecodable character
# in a comment, though Python will run them. So we'll change the
@@ -459,7 +472,7 @@
expected = "# Isn't this great?�!"
assert expected in html_report
- def test_formfeeds(self):
+ def test_formfeeds(self) -> None:
# https://github.com/nedbat/coveragepy/issues/360
self.make_file("formfeed.py", "line_one = 1\n\f\nline_two = 2\n")
cov = coverage.Coverage()
@@ -469,11 +482,43 @@
formfeed_html = self.get_html_report_content("formfeed.py")
assert "line_two" in formfeed_html
+ def test_splitlines_special_chars(self) -> None:
+ # https://github.com/nedbat/coveragepy/issues/1512
+ # See https://docs.python.org/3/library/stdtypes.html#str.splitlines for
+ # the characters splitlines treats specially that readlines does not.
+
+ # I'm not exactly sure why we need the "a" strings here, but the old
+ # code wasn't failing without them.
+ self.make_file("splitlines_is_weird.py", """\
+ test = {
+ "0b": ["\x0b0"], "a1": "this is line 2",
+ "0c": ["\x0c0"], "a2": "this is line 3",
+ "1c": ["\x1c0"], "a3": "this is line 4",
+ "1d": ["\x1d0"], "a4": "this is line 5",
+ "1e": ["\x1e0"], "a5": "this is line 6",
+ "85": ["\x850"], "a6": "this is line 7",
+ "2028": ["\u20280"], "a7": "this is line 8",
+ "2029": ["\u20290"], "a8": "this is line 9",
+ }
+ DONE = 1
+ """)
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "splitlines_is_weird")
+ cov.html_report()
+
+ the_html = self.get_html_report_content("splitlines_is_weird.py")
+ assert "DONE" in the_html
+
+ # Check that the lines are properly decoded and reported...
+ html_lines = the_html.split("\n")
+ assert any(re.search(r'id="t2".*"this is line 2"', line) for line in html_lines)
+ assert any(re.search(r'id="t9".*"this is line 9"', line) for line in html_lines)
+
class HtmlTest(HtmlTestHelpers, CoverageTest):
"""Moar HTML tests."""
- def test_missing_source_file_incorrect_message(self):
+ def test_missing_source_file_incorrect_message(self) -> None:
# https://github.com/nedbat/coveragepy/issues/60
self.make_file("thefile.py", "import sub.another\n")
self.make_file("sub/__init__.py", "")
@@ -488,7 +533,7 @@
with pytest.raises(NoSource, match=msg):
cov.html_report()
- def test_extensionless_file_collides_with_extension(self):
+ def test_extensionless_file_collides_with_extension(self) -> None:
# It used to be that "program" and "program.py" would both be reported
# to "program.html". Now they are not.
# https://github.com/nedbat/coveragepy/issues/69
@@ -505,7 +550,7 @@
self.assert_exists("htmlcov/program.html")
self.assert_exists("htmlcov/program_py.html")
- def test_has_date_stamp_in_files(self):
+ def test_has_date_stamp_in_files(self) -> None:
self.create_initial_files()
self.run_coverage()
@@ -514,7 +559,7 @@
with open("htmlcov/main_file_py.html") as f:
self.assert_correct_timestamp(f.read())
- def test_reporting_on_unmeasured_file(self):
+ def test_reporting_on_unmeasured_file(self) -> None:
# It should be ok to ask for an HTML report on a file that wasn't even
# measured at all. https://github.com/nedbat/coveragepy/issues/403
self.create_initial_files()
@@ -523,7 +568,7 @@
self.assert_exists("htmlcov/index.html")
self.assert_exists("htmlcov/other_py.html")
- def make_main_and_not_covered(self):
+ def make_main_and_not_covered(self) -> None:
"""Helper to create files for skip_covered scenarios."""
self.make_file("main_file.py", """
import not_covered
@@ -537,14 +582,14 @@
print("n")
""")
- def test_report_skip_covered(self):
+ def test_report_skip_covered(self) -> None:
self.make_main_and_not_covered()
self.run_coverage(htmlargs=dict(skip_covered=True))
self.assert_exists("htmlcov/index.html")
self.assert_doesnt_exist("htmlcov/main_file_py.html")
self.assert_exists("htmlcov/not_covered_py.html")
- def test_html_skip_covered(self):
+ def test_html_skip_covered(self) -> None:
self.make_main_and_not_covered()
self.make_file(".coveragerc", "[html]\nskip_covered = True")
self.run_coverage()
@@ -554,14 +599,14 @@
index = self.get_html_index_content()
assert "1 file skipped due to complete coverage." in index
- def test_report_skip_covered_branches(self):
+ def test_report_skip_covered_branches(self) -> None:
self.make_main_and_not_covered()
self.run_coverage(covargs=dict(branch=True), htmlargs=dict(skip_covered=True))
self.assert_exists("htmlcov/index.html")
self.assert_doesnt_exist("htmlcov/main_file_py.html")
self.assert_exists("htmlcov/not_covered_py.html")
- def test_report_skip_covered_100(self):
+ def test_report_skip_covered_100(self) -> None:
self.make_file("main_file.py", """
def normal():
print("z")
@@ -571,7 +616,7 @@
assert res == 100.0
self.assert_doesnt_exist("htmlcov/main_file_py.html")
- def make_init_and_main(self):
+ def make_init_and_main(self) -> None:
"""Helper to create files for skip_empty scenarios."""
self.make_file("submodule/__init__.py", "")
self.make_file("main_file.py", """
@@ -582,7 +627,7 @@
normal()
""")
- def test_report_skip_empty(self):
+ def test_report_skip_empty(self) -> None:
self.make_init_and_main()
self.run_coverage(htmlargs=dict(skip_empty=True))
self.assert_exists("htmlcov/index.html")
@@ -591,7 +636,7 @@
index = self.get_html_index_content()
assert "1 empty file skipped." in index
- def test_html_skip_empty(self):
+ def test_html_skip_empty(self) -> None:
self.make_init_and_main()
self.make_file(".coveragerc", "[html]\nskip_empty = True")
self.run_coverage()
@@ -600,7 +645,7 @@
self.assert_doesnt_exist("htmlcov/submodule___init___py.html")
-def filepath_to_regex(path):
+def filepath_to_regex(path: str) -> str:
"""Create a regex for scrubbing a file path."""
regex = re.escape(path)
# If there's a backslash, let it match either slash.
@@ -610,12 +655,16 @@
return regex
-def compare_html(expected, actual, extra_scrubs=None):
+def compare_html(
+ expected: str,
+ actual: str,
+ extra_scrubs: Optional[List[Tuple[str, str]]] = None,
+) -> None:
"""Specialized compare function for our HTML files."""
__tracebackhide__ = True # pytest, please don't show me this function.
scrubs = [
- (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'),
- (r'coverage.py v[\d.abc]+', 'coverage.py vVER'),
+ (r'/coverage\.readthedocs\.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'),
+ (r'coverage\.py v[\d.abcdev]+', 'coverage.py vVER'),
(r'created at \d\d\d\d-\d\d-\d\d \d\d:\d\d [-+]\d\d\d\d', 'created at DATE'),
(r'created at \d\d\d\d-\d\d-\d\d \d\d:\d\d', 'created at DATE'),
# Occasionally an absolute path is in the HTML report.
@@ -639,7 +688,7 @@
class HtmlGoldTest(CoverageTest):
"""Tests of HTML reporting that use gold files."""
- def test_a(self):
+ def test_a(self) -> None:
self.make_file("a.py", """\
if 1 < 2:
# Needed a < to look at HTML entities.
@@ -668,7 +717,7 @@
'67% ',
)
- def test_b_branch(self):
+ def test_b_branch(self) -> None:
self.make_file("b.py", """\
def one(x):
# This will be a branch that misses the else.
@@ -733,7 +782,7 @@
'70% ',
)
- def test_bom(self):
+ def test_bom(self) -> None:
self.make_file("bom.py", bytes=b"""\
\xef\xbb\xbf# A Python source file in utf-8, with BOM.
math = "3\xc3\x974 = 12, \xc3\xb72 = 6\xc2\xb10"
@@ -766,7 +815,7 @@
'"3×4 = 12, ÷2 = 6±0"',
)
- def test_isolatin1(self):
+ def test_isolatin1(self) -> None:
self.make_file("isolatin1.py", bytes=b"""\
# -*- coding: iso8859-1 -*-
# A Python source file in another encoding.
@@ -785,7 +834,7 @@
'"3×4 = 12, ÷2 = 6±0"',
)
- def make_main_etc(self):
+ def make_main_etc(self) -> None:
"""Make main.py and m1-m3.py for other tests."""
self.make_file("main.py", """\
import m1
@@ -812,28 +861,28 @@
m3b = 2
""")
- def test_omit_1(self):
+ def test_omit_1(self) -> None:
self.make_main_etc()
cov = coverage.Coverage(include=["./*"])
self.start_import_stop(cov, "main")
cov.html_report(directory="out/omit_1")
compare_html(gold_path("html/omit_1"), "out/omit_1")
- def test_omit_2(self):
+ def test_omit_2(self) -> None:
self.make_main_etc()
cov = coverage.Coverage(include=["./*"])
self.start_import_stop(cov, "main")
cov.html_report(directory="out/omit_2", omit=["m1.py"])
compare_html(gold_path("html/omit_2"), "out/omit_2")
- def test_omit_3(self):
+ def test_omit_3(self) -> None:
self.make_main_etc()
cov = coverage.Coverage(include=["./*"])
self.start_import_stop(cov, "main")
cov.html_report(directory="out/omit_3", omit=["m1.py", "m2.py"])
compare_html(gold_path("html/omit_3"), "out/omit_3")
- def test_omit_4(self):
+ def test_omit_4(self) -> None:
self.make_main_etc()
self.make_file("omit4.ini", """\
[report]
@@ -845,7 +894,7 @@
cov.html_report(directory="out/omit_4")
compare_html(gold_path("html/omit_4"), "out/omit_4")
- def test_omit_5(self):
+ def test_omit_5(self) -> None:
self.make_main_etc()
self.make_file("omit5.ini", """\
[report]
@@ -863,7 +912,7 @@
cov.html_report()
compare_html(gold_path("html/omit_5"), "out/omit_5")
- def test_other(self):
+ def test_other(self) -> None:
self.make_file("src/here.py", """\
import other
@@ -903,7 +952,7 @@
'other.py',
)
- def test_partial(self):
+ def test_partial(self) -> None:
self.make_file("partial.py", """\
# partial branches and excluded lines
a = 2
@@ -970,7 +1019,7 @@
'91%',
)
- def test_styled(self):
+ def test_styled(self) -> None:
self.make_file("a.py", """\
if 1 < 2:
# Needed a < to look at HTML entities.
@@ -1003,7 +1052,7 @@
'67%',
)
- def test_tabbed(self):
+ def test_tabbed(self) -> None:
# The file contents would look like this with 8-space tabs:
# x = 1
# if x:
@@ -1037,7 +1086,7 @@
doesnt_contain("out/tabbed_py.html", "\t")
- def test_unicode(self):
+ def test_unicode(self) -> None:
surrogate = "\U000e0100"
self.make_file("unicode.py", """\
@@ -1064,7 +1113,7 @@
'"db40,dd00: x󠄀"',
)
- def test_accented_dot_py(self):
+ def test_accented_dot_py(self) -> None:
# Make a file with a non-ascii character in the filename.
self.make_file("h\xe2t.py", "print('accented')")
self.make_data_file(lines={abs_file("h\xe2t.py"): [1]})
@@ -1076,7 +1125,7 @@
index = indexf.read()
assert 'hât.py' in index
- def test_accented_directory(self):
+ def test_accented_directory(self) -> None:
# Make a file with a non-ascii character in the directory name.
self.make_file("\xe2/accented.py", "print('accented')")
self.make_data_file(lines={abs_file("\xe2/accented.py"): [1]})
@@ -1097,7 +1146,7 @@
EMPTY = coverage.html.HtmlDataGeneration.EMPTY
- def html_data_from_cov(self, cov, morf):
+ def html_data_from_cov(self, cov: Coverage, morf: TMorf) -> coverage.html.FileData:
"""Get HTML report data from a `Coverage` object for a morf."""
with self.assert_warnings(cov, []):
datagen = coverage.html.HtmlDataGeneration(cov)
@@ -1134,7 +1183,7 @@
TEST_ONE_LINES = [5, 6, 2]
TEST_TWO_LINES = [9, 10, 11, 13, 14, 15, 2]
- def test_dynamic_contexts(self):
+ def test_dynamic_contexts(self) -> None:
self.make_file("two_tests.py", self.SOURCE)
cov = coverage.Coverage(source=["."])
cov.set_option("run:dynamic_context", "test_function")
@@ -1150,7 +1199,10 @@
]
assert sorted(expected) == sorted(actual)
- def test_filtered_dynamic_contexts(self):
+ cov.html_report(mod, directory="out/contexts")
+ compare_html(gold_path("html/contexts"), "out/contexts")
+
+ def test_filtered_dynamic_contexts(self) -> None:
self.make_file("two_tests.py", self.SOURCE)
cov = coverage.Coverage(source=["."])
cov.set_option("run:dynamic_context", "test_function")
@@ -1160,12 +1212,12 @@
d = self.html_data_from_cov(cov, mod)
context_labels = [self.EMPTY, 'two_tests.test_one', 'two_tests.test_two']
- expected_lines = [[], self.TEST_ONE_LINES, []]
+ expected_lines: List[List[TLineNo]] = [[], self.TEST_ONE_LINES, []]
for label, expected in zip(context_labels, expected_lines):
actual = [ld.number for ld in d.lines if label in (ld.contexts or ())]
assert sorted(expected) == sorted(actual)
- def test_no_contexts_warns_no_contexts(self):
+ def test_no_contexts_warns_no_contexts(self) -> None:
# If no contexts were collected, then show_contexts emits a warning.
self.make_file("two_tests.py", self.SOURCE)
cov = coverage.Coverage(source=["."])
@@ -1174,7 +1226,7 @@
with self.assert_warnings(cov, ["No contexts were measured"]):
cov.html_report()
- def test_dynamic_contexts_relative_files(self):
+ def test_dynamic_contexts_relative_files(self) -> None:
self.make_file("two_tests.py", self.SOURCE)
self.make_file("config", "[run]\nrelative_files = True")
cov = coverage.Coverage(source=["."], config_file="config")
@@ -1195,16 +1247,25 @@
class HtmlHelpersTest(HtmlTestHelpers, CoverageTest):
"""Tests of the helpers in HtmlTestHelpers."""
- def test_bad_link(self):
+ def test_bad_link(self) -> None:
# Does assert_valid_hrefs detect links to non-existent files?
self.make_file("htmlcov/index.html", "Nothing")
msg = "These files link to 'nothing.html', which doesn't exist: htmlcov.index.html"
with pytest.raises(AssertionError, match=msg):
self.assert_valid_hrefs()
- def test_bad_anchor(self):
+ def test_bad_anchor(self) -> None:
# Does assert_valid_hrefs detect fragments that go nowhere?
self.make_file("htmlcov/index.html", "Nothing")
msg = "Fragment '#nothing' in htmlcov.index.html has no anchor"
with pytest.raises(AssertionError, match=msg):
self.assert_valid_hrefs()
+
+
+@pytest.mark.parametrize("n, key", [
+ (0, "a"),
+ (1, "b"),
+ (999999999, "e9S_p"),
+])
+def test_encode_int(n: int, key: str) -> None:
+ assert coverage.html.encode_int(n) == key
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_json.py python-coverage-7.2.7+dfsg1/tests/test_json.py
--- python-coverage-6.5.0+dfsg1/tests/test_json.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_json.py 2023-05-29 19:46:30.000000000 +0000
@@ -2,17 +2,29 @@
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
"""Test json-based summary reporting for coverage.py"""
-from datetime import datetime
+
+from __future__ import annotations
+
import json
import os
+from datetime import datetime
+from typing import Any, Dict
+
import coverage
+from coverage import Coverage
+
from tests.coveragetest import UsingModulesMixin, CoverageTest
class JsonReportTest(UsingModulesMixin, CoverageTest):
"""Tests of the JSON reports from coverage.py."""
- def _assert_expected_json_report(self, cov, expected_result):
+
+ def _assert_expected_json_report(
+ self,
+ cov: Coverage,
+ expected_result: Dict[str, Any],
+ ) -> None:
"""
Helper for tests that handles the common ceremony so the tests can be clearly show the
consequences of setting various arguments.
@@ -39,7 +51,7 @@
del (parsed_result['meta']['timestamp'])
assert parsed_result == expected_result
- def test_branch_coverage(self):
+ def test_branch_coverage(self) -> None:
cov = coverage.Coverage(branch=True)
expected_result = {
'meta': {
@@ -91,7 +103,7 @@
}
self._assert_expected_json_report(cov, expected_result)
- def test_simple_line_coverage(self):
+ def test_simple_line_coverage(self) -> None:
cov = coverage.Coverage()
expected_result = {
'meta': {
@@ -125,7 +137,7 @@
}
self._assert_expected_json_report(cov, expected_result)
- def run_context_test(self, relative_files):
+ def run_context_test(self, relative_files: bool) -> None:
"""A helper for two tests below."""
self.make_file("config", """\
[run]
@@ -187,8 +199,8 @@
}
self._assert_expected_json_report(cov, expected_result)
- def test_context_non_relative(self):
+ def test_context_non_relative(self) -> None:
self.run_context_test(relative_files=False)
- def test_context_relative(self):
+ def test_context_relative(self) -> None:
self.run_context_test(relative_files=True)
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_lcov.py python-coverage-7.2.7+dfsg1/tests/test_lcov.py
--- python-coverage-6.5.0+dfsg1/tests/test_lcov.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_lcov.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,9 @@
"""Test LCOV-based summary reporting for coverage.py."""
+from __future__ import annotations
+
+import math
import textwrap
from tests.coveragetest import CoverageTest
@@ -14,14 +17,12 @@
class LcovTest(CoverageTest):
"""Tests of the LCOV reports from coverage.py."""
- def create_initial_files(self):
+ def create_initial_files(self) -> None:
"""
Helper for tests that handles the common ceremony so the tests can
show the consequences of changes in the setup.
"""
self.make_file("main_file.py", """\
- #!/usr/bin/env python3
-
def cuboid_volume(l):
return (l*l*l)
@@ -30,8 +31,6 @@
""")
self.make_file("test_file.py", """\
- #!/usr/bin/env python3
-
from main_file import cuboid_volume
import unittest
@@ -43,18 +42,15 @@
self.assertAlmostEqual(cuboid_volume(5.5),166.375)
""")
- def get_lcov_report_content(self, filename="coverage.lcov"):
+ def get_lcov_report_content(self, filename: str = "coverage.lcov") -> str:
"""Return the content of an LCOV report."""
with open(filename, "r") as file:
- file_contents = file.read()
- return file_contents
+ return file.read()
- def test_lone_file(self):
- """For a single file with a couple of functions, the lcov should cover
- the function definitions themselves, but not the returns."""
+ def test_lone_file(self) -> None:
+ # For a single file with a couple of functions, the lcov should cover
+ # the function definitions themselves, but not the returns.
self.make_file("main_file.py", """\
- #!/usr/bin/env python3
-
def cuboid_volume(l):
return (l*l*l)
@@ -64,10 +60,10 @@
expected_result = textwrap.dedent("""\
TN:
SF:main_file.py
- DA:3,1,7URou3io0zReBkk69lEb/Q
- DA:6,1,ilhb4KUfytxtEuClijZPlQ
- DA:4,0,Xqj6H1iz/nsARMCAbE90ng
- DA:7,0,LWILTcvARcydjFFyo9qM0A
+ DA:1,1,7URou3io0zReBkk69lEb/Q
+ DA:4,1,ilhb4KUfytxtEuClijZPlQ
+ DA:2,0,Xqj6H1iz/nsARMCAbE90ng
+ DA:5,0,LWILTcvARcydjFFyo9qM0A
LF:4
LH:2
end_of_record
@@ -75,40 +71,42 @@
self.assert_doesnt_exist(".coverage")
cov = coverage.Coverage(source=["."])
self.start_import_stop(cov, "main_file")
- cov.lcov_report()
+ pct = cov.lcov_report()
+ assert pct == 50.0
actual_result = self.get_lcov_report_content()
assert expected_result == actual_result
- def test_simple_line_coverage_two_files(self):
- """Test that line coverage is created when coverage is run,
- and matches the output of the file below."""
+ def test_simple_line_coverage_two_files(self) -> None:
+ # Test that line coverage is created when coverage is run,
+ # and matches the output of the file below.
self.create_initial_files()
self.assert_doesnt_exist(".coverage")
self.make_file(".coveragerc", "[lcov]\noutput = data.lcov\n")
cov = coverage.Coverage(source=".")
self.start_import_stop(cov, "test_file")
- cov.lcov_report()
+ pct = cov.lcov_report()
+ assert pct == 50.0
self.assert_exists("data.lcov")
expected_result = textwrap.dedent("""\
TN:
SF:main_file.py
- DA:3,1,7URou3io0zReBkk69lEb/Q
- DA:6,1,ilhb4KUfytxtEuClijZPlQ
- DA:4,0,Xqj6H1iz/nsARMCAbE90ng
- DA:7,0,LWILTcvARcydjFFyo9qM0A
+ DA:1,1,7URou3io0zReBkk69lEb/Q
+ DA:4,1,ilhb4KUfytxtEuClijZPlQ
+ DA:2,0,Xqj6H1iz/nsARMCAbE90ng
+ DA:5,0,LWILTcvARcydjFFyo9qM0A
LF:4
LH:2
end_of_record
TN:
SF:test_file.py
- DA:3,1,R5Rb4IzmjKRgY/vFFc1TRg
- DA:4,1,E/tvV9JPVDhEcTCkgrwOFw
- DA:6,1,GP08LPBYJq8EzYveHJy2qA
- DA:7,1,MV+jSLi6PFEl+WatEAptog
- DA:8,0,qyqd1mF289dg6oQAQHA+gQ
- DA:9,0,nmEYd5F1KrxemgC9iVjlqg
- DA:10,0,jodMK26WYDizOO1C7ekBbg
- DA:11,0,LtxfKehkX8o4KvC5GnN52g
+ DA:1,1,R5Rb4IzmjKRgY/vFFc1TRg
+ DA:2,1,E/tvV9JPVDhEcTCkgrwOFw
+ DA:4,1,GP08LPBYJq8EzYveHJy2qA
+ DA:5,1,MV+jSLi6PFEl+WatEAptog
+ DA:6,0,qyqd1mF289dg6oQAQHA+gQ
+ DA:7,0,nmEYd5F1KrxemgC9iVjlqg
+ DA:8,0,jodMK26WYDizOO1C7ekBbg
+ DA:9,0,LtxfKehkX8o4KvC5GnN52g
LF:8
LH:4
end_of_record
@@ -116,11 +114,9 @@
actual_result = self.get_lcov_report_content(filename="data.lcov")
assert expected_result == actual_result
- def test_branch_coverage_one_file(self):
- """Test that the reporter produces valid branch coverage."""
+ def test_branch_coverage_one_file(self) -> None:
+ # Test that the reporter produces valid branch coverage.
self.make_file("main_file.py", """\
- #!/usr/bin/env python3
-
def is_it_x(x):
if x == 3:
return x
@@ -130,19 +126,20 @@
self.assert_doesnt_exist(".coverage")
cov = coverage.Coverage(branch=True, source=".")
self.start_import_stop(cov, "main_file")
- cov.lcov_report()
+ pct = cov.lcov_report()
+ assert math.isclose(pct, 16.666666666666668)
self.assert_exists("coverage.lcov")
expected_result = textwrap.dedent("""\
TN:
SF:main_file.py
- DA:3,1,4MDXMbvwQ3L7va1tsphVzw
- DA:4,0,MuERA6EYyZNpKPqoJfzwkA
- DA:5,0,sAyiiE6iAuPMte9kyd0+3g
- DA:7,0,W/g8GJDAYJkSSurt59Mzfw
+ DA:1,1,4MDXMbvwQ3L7va1tsphVzw
+ DA:2,0,MuERA6EYyZNpKPqoJfzwkA
+ DA:3,0,sAyiiE6iAuPMte9kyd0+3g
+ DA:5,0,W/g8GJDAYJkSSurt59Mzfw
LF:4
LH:1
- BRDA:5,0,0,-
- BRDA:7,0,1,-
+ BRDA:3,0,0,-
+ BRDA:5,0,1,-
BRF:2
BRH:0
end_of_record
@@ -150,12 +147,10 @@
actual_result = self.get_lcov_report_content()
assert expected_result == actual_result
- def test_branch_coverage_two_files(self):
- """Test that valid branch coverage is generated
- in the case of two files."""
+ def test_branch_coverage_two_files(self) -> None:
+ # Test that valid branch coverage is generated
+ # in the case of two files.
self.make_file("main_file.py", """\
- #!/usr/bin/env python3
-
def is_it_x(x):
if x == 3:
return x
@@ -164,8 +159,6 @@
""")
self.make_file("test_file.py", """\
- #!/usr/bin/env python3
-
from main_file import *
import unittest
@@ -177,30 +170,31 @@
self.assert_doesnt_exist(".coverage")
cov = coverage.Coverage(branch=True, source=".")
self.start_import_stop(cov, "test_file")
- cov.lcov_report()
+ pct = cov.lcov_report()
+ assert math.isclose(pct, 41.666666666666664)
self.assert_exists("coverage.lcov")
expected_result = textwrap.dedent("""\
TN:
SF:main_file.py
- DA:3,1,4MDXMbvwQ3L7va1tsphVzw
- DA:4,0,MuERA6EYyZNpKPqoJfzwkA
- DA:5,0,sAyiiE6iAuPMte9kyd0+3g
- DA:7,0,W/g8GJDAYJkSSurt59Mzfw
+ DA:1,1,4MDXMbvwQ3L7va1tsphVzw
+ DA:2,0,MuERA6EYyZNpKPqoJfzwkA
+ DA:3,0,sAyiiE6iAuPMte9kyd0+3g
+ DA:5,0,W/g8GJDAYJkSSurt59Mzfw
LF:4
LH:1
- BRDA:5,0,0,-
- BRDA:7,0,1,-
+ BRDA:3,0,0,-
+ BRDA:5,0,1,-
BRF:2
BRH:0
end_of_record
TN:
SF:test_file.py
- DA:3,1,9TxKIyoBtmhopmlbDNa8FQ
- DA:4,1,E/tvV9JPVDhEcTCkgrwOFw
- DA:6,1,C3s/c8C1Yd/zoNG1GnGexg
- DA:7,1,9qPyWexYysgeKtB+YvuzAg
- DA:8,0,LycuNcdqoUhPXeuXUTf5lA
- DA:9,0,FPTWzd68bDx76HN7VHu1wA
+ DA:1,1,9TxKIyoBtmhopmlbDNa8FQ
+ DA:2,1,E/tvV9JPVDhEcTCkgrwOFw
+ DA:4,1,C3s/c8C1Yd/zoNG1GnGexg
+ DA:5,1,9qPyWexYysgeKtB+YvuzAg
+ DA:6,0,LycuNcdqoUhPXeuXUTf5lA
+ DA:7,0,FPTWzd68bDx76HN7VHu1wA
LF:6
LH:4
BRF:0
@@ -210,10 +204,9 @@
actual_result = self.get_lcov_report_content()
assert actual_result == expected_result
- def test_half_covered_branch(self):
- """Test that for a given branch that is only half covered,
- the block numbers remain the same, and produces valid lcov.
- """
+ def test_half_covered_branch(self) -> None:
+ # Test that for a given branch that is only half covered,
+ # the block numbers remain the same, and produces valid lcov.
self.make_file("main_file.py", """\
something = True
@@ -225,7 +218,8 @@
self.assert_doesnt_exist(".coverage")
cov = coverage.Coverage(branch=True, source=".")
self.start_import_stop(cov, "main_file")
- cov.lcov_report()
+ pct = cov.lcov_report()
+ assert math.isclose(pct, 66.66666666666667)
self.assert_exists("coverage.lcov")
expected_result = textwrap.dedent("""\
TN:
@@ -245,21 +239,21 @@
actual_result = self.get_lcov_report_content()
assert actual_result == expected_result
- def test_empty_init_files(self):
- """Test that in the case of an empty __init__.py file, the lcov
- reporter will note that the file is there, and will note the empty
- line. It will also note the lack of branches, and the checksum for
- the line.
-
- Although there are no lines found, it will note one line as hit in
- old Pythons, and no lines hit in newer Pythons.
- """
+ def test_empty_init_files(self) -> None:
+ # Test that in the case of an empty __init__.py file, the lcov
+ # reporter will note that the file is there, and will note the empty
+ # line. It will also note the lack of branches, and the checksum for
+ # the line.
+ #
+ # Although there are no lines found, it will note one line as hit in
+ # old Pythons, and no lines hit in newer Pythons.
self.make_file("__init__.py", "")
self.assert_doesnt_exist(".coverage")
cov = coverage.Coverage(branch=True, source=".")
self.start_import_stop(cov, "__init__")
- cov.lcov_report()
+ pct = cov.lcov_report()
+ assert pct == 0.0
self.assert_exists("coverage.lcov")
# Newer Pythons have truly empty empty files.
if env.PYBEHAVIOR.empty_is_empty:
@@ -278,7 +272,7 @@
SF:__init__.py
DA:1,1,1B2M2Y8AsgTpgAmY7PhCfg
LF:0
- LH:1
+ LH:0
BRF:0
BRH:0
end_of_record
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_misc.py python-coverage-7.2.7+dfsg1/tests/test_misc.py
--- python-coverage-6.5.0+dfsg1/tests/test_misc.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_misc.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,15 +3,17 @@
"""Tests of miscellaneous stuff."""
+from __future__ import annotations
+
import sys
+from unittest import mock
import pytest
-from coverage import env
from coverage.exceptions import CoverageException
-from coverage.misc import contract, dummy_decorator_with_args, file_be_gone
-from coverage.misc import Hasher, one_of, substitute_variables, import_third_party
-from coverage.misc import human_sorted, human_sorted_items
+from coverage.misc import file_be_gone
+from coverage.misc import Hasher, substitute_variables, import_third_party
+from coverage.misc import human_sorted, human_sorted_items, stdout_link
from tests.coveragetest import CoverageTest
@@ -21,7 +23,7 @@
run_in_temp_dir = False
- def test_string_hashing(self):
+ def test_string_hashing(self) -> None:
h1 = Hasher()
h1.update("Hello, world!")
h2 = Hasher()
@@ -31,28 +33,28 @@
assert h1.hexdigest() != h2.hexdigest()
assert h1.hexdigest() == h3.hexdigest()
- def test_bytes_hashing(self):
+ def test_bytes_hashing(self) -> None:
h1 = Hasher()
h1.update(b"Hello, world!")
h2 = Hasher()
h2.update(b"Goodbye!")
assert h1.hexdigest() != h2.hexdigest()
- def test_unicode_hashing(self):
+ def test_unicode_hashing(self) -> None:
h1 = Hasher()
h1.update("Hello, world! \N{SNOWMAN}")
h2 = Hasher()
h2.update("Goodbye!")
assert h1.hexdigest() != h2.hexdigest()
- def test_dict_hashing(self):
+ def test_dict_hashing(self) -> None:
h1 = Hasher()
h1.update({'a': 17, 'b': 23})
h2 = Hasher()
h2.update({'b': 23, 'a': 17})
assert h1.hexdigest() == h2.hexdigest()
- def test_dict_collision(self):
+ def test_dict_collision(self) -> None:
h1 = Hasher()
h1.update({'a': 17, 'b': {'c': 1, 'd': 2}})
h2 = Hasher()
@@ -63,73 +65,23 @@
class RemoveFileTest(CoverageTest):
"""Tests of misc.file_be_gone."""
- def test_remove_nonexistent_file(self):
+ def test_remove_nonexistent_file(self) -> None:
# It's OK to try to remove a file that doesn't exist.
file_be_gone("not_here.txt")
- def test_remove_actual_file(self):
+ def test_remove_actual_file(self) -> None:
# It really does remove a file that does exist.
self.make_file("here.txt", "We are here, we are here, we are here!")
file_be_gone("here.txt")
self.assert_doesnt_exist("here.txt")
- def test_actual_errors(self):
+ def test_actual_errors(self) -> None:
# Errors can still happen.
# ". is a directory" on Unix, or "Access denied" on Windows
with pytest.raises(OSError):
file_be_gone(".")
-@pytest.mark.skipif(not env.USE_CONTRACTS, reason="Contracts are disabled, can't test them")
-class ContractTest(CoverageTest):
- """Tests of our contract decorators."""
-
- run_in_temp_dir = False
-
- def test_bytes(self):
- @contract(text='bytes|None')
- def need_bytes(text=None):
- return text
-
- assert need_bytes(b"Hey") == b"Hey"
- assert need_bytes() is None
- with pytest.raises(Exception):
- need_bytes("Oops")
-
- def test_unicode(self):
- @contract(text='unicode|None')
- def need_unicode(text=None):
- return text
-
- assert need_unicode("Hey") == "Hey"
- assert need_unicode() is None
- with pytest.raises(Exception):
- need_unicode(b"Oops")
-
- def test_one_of(self):
- @one_of("a, b, c")
- def give_me_one(a=None, b=None, c=None):
- return (a, b, c)
-
- assert give_me_one(a=17) == (17, None, None)
- assert give_me_one(b=set()) == (None, set(), None)
- assert give_me_one(c=17) == (None, None, 17)
- with pytest.raises(AssertionError):
- give_me_one(a=17, b=set())
- with pytest.raises(AssertionError):
- give_me_one()
-
- def test_dummy_decorator_with_args(self):
- @dummy_decorator_with_args("anything", this=17, that="is fine")
- def undecorated(a=None, b=None):
- return (a, b)
-
- assert undecorated() == (None, None)
- assert undecorated(17) == (17, None)
- assert undecorated(b=23) == (None, 23)
- assert undecorated(b=42, a=3) == (3, 42)
-
-
VARS = {
'FOO': 'fooey',
'BAR': 'xyzzy',
@@ -147,13 +99,13 @@
("Defaulted: ${WUT-missing}!", "Defaulted: missing!"),
("Defaulted empty: ${WUT-}!", "Defaulted empty: !"),
])
-def test_substitute_variables(before, after):
+def test_substitute_variables(before: str, after: str) -> None:
assert substitute_variables(before, VARS) == after
@pytest.mark.parametrize("text", [
"Strict: ${NOTHING?} is an error",
])
-def test_substitute_variables_errors(text):
+def test_substitute_variables_errors(text: str) -> None:
with pytest.raises(CoverageException) as exc_info:
substitute_variables(text, VARS)
assert text in str(exc_info.value)
@@ -165,11 +117,12 @@
run_in_temp_dir = False
- def test_success(self):
+ def test_success(self) -> None:
# Make sure we don't have pytest in sys.modules before we start.
del sys.modules["pytest"]
# Import pytest
- mod = import_third_party("pytest")
+ mod, has = import_third_party("pytest")
+ assert has
# Yes, it's really pytest:
assert mod.__name__ == "pytest"
print(dir(mod))
@@ -177,9 +130,9 @@
# But it's not in sys.modules:
assert "pytest" not in sys.modules
- def test_failure(self):
- mod = import_third_party("xyzzy")
- assert mod is None
+ def test_failure(self) -> None:
+ _, has = import_third_party("xyzzy")
+ assert not has
assert "xyzzy" not in sys.modules
@@ -190,14 +143,32 @@
]
@pytest.mark.parametrize("words, ordered", HUMAN_DATA)
-def test_human_sorted(words, ordered):
+def test_human_sorted(words: str, ordered: str) -> None:
assert " ".join(human_sorted(words.split())) == ordered
@pytest.mark.parametrize("words, ordered", HUMAN_DATA)
-def test_human_sorted_items(words, ordered):
+def test_human_sorted_items(words: str, ordered: str) -> None:
keys = words.split()
items = [(k, 1) for k in keys] + [(k, 2) for k in keys]
okeys = ordered.split()
oitems = [(k, v) for k in okeys for v in [1, 2]]
assert human_sorted_items(items) == oitems
assert human_sorted_items(items, reverse=True) == oitems[::-1]
+
+
+def test_stdout_link_tty() -> None:
+ with mock.patch.object(sys.stdout, "isatty", lambda:True):
+ link = stdout_link("some text", "some url")
+ assert link == "\033]8;;some url\asome text\033]8;;\a"
+
+
+def test_stdout_link_not_tty() -> None:
+ # Without mocking isatty, it reports False in a pytest suite.
+ assert stdout_link("some text", "some url") == "some text"
+
+
+def test_stdout_link_with_fake_stdout() -> None:
+ # If stdout is another object, we should still be ok.
+ with mock.patch.object(sys, "stdout", object()):
+ link = stdout_link("some text", "some url")
+ assert link == "some text"
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_mixins.py python-coverage-7.2.7+dfsg1/tests/test_mixins.py
--- python-coverage-6.5.0+dfsg1/tests/test_mixins.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_mixins.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,8 @@
"""Tests of code in tests/mixins.py"""
+from __future__ import annotations
+
import pytest
from coverage.misc import import_local_file
@@ -13,12 +15,12 @@
class TempDirMixinTest(TempDirMixin):
"""Test the methods in TempDirMixin."""
- def file_text(self, fname):
+ def file_text(self, fname: str) -> str:
"""Return the text read from a file."""
with open(fname, "rb") as f:
return f.read().decode('ascii')
- def test_make_file(self):
+ def test_make_file(self) -> None:
# A simple file.
self.make_file("fooey.boo", "Hello there")
assert self.file_text("fooey.boo") == "Hello there"
@@ -38,7 +40,7 @@
""")
assert self.file_text("dedented.txt") == "Hello\nBye\n"
- def test_make_file_newline(self):
+ def test_make_file_newline(self) -> None:
self.make_file("unix.txt", "Hello\n")
assert self.file_text("unix.txt") == "Hello\n"
self.make_file("dos.txt", "Hello\n", newline="\r\n")
@@ -46,13 +48,13 @@
self.make_file("mac.txt", "Hello\n", newline="\r")
assert self.file_text("mac.txt") == "Hello\r"
- def test_make_file_non_ascii(self):
+ def test_make_file_non_ascii(self) -> None:
self.make_file("unicode.txt", "tablo: «ταБℓσ»")
with open("unicode.txt", "rb") as f:
text = f.read()
assert text == b"tablo: \xc2\xab\xcf\x84\xce\xb1\xd0\x91\xe2\x84\x93\xcf\x83\xc2\xbb"
- def test_make_bytes_file(self):
+ def test_make_bytes_file(self) -> None:
self.make_file("binary.dat", bytes=b"\x99\x33\x66hello\0")
with open("binary.dat", "rb") as f:
data = f.read()
@@ -63,12 +65,12 @@
"""Tests of SysPathModulesMixin."""
@pytest.mark.parametrize("val", [17, 42])
- def test_module_independence(self, val):
+ def test_module_independence(self, val: int) -> None:
self.make_file("xyzzy.py", f"A = {val}")
import xyzzy # pylint: disable=import-error
assert xyzzy.A == val
- def test_cleanup_and_reimport(self):
+ def test_cleanup_and_reimport(self) -> None:
self.make_file("xyzzy.py", "A = 17")
xyzzy = import_local_file("xyzzy")
assert xyzzy.A == 17
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_numbits.py python-coverage-7.2.7+dfsg1/tests/test_numbits.py
--- python-coverage-6.5.0+dfsg1/tests/test_numbits.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_numbits.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,9 +3,13 @@
"""Tests for coverage.numbits"""
+from __future__ import annotations
+
import json
import sqlite3
+from typing import Iterable, Set
+
from hypothesis import example, given, settings
from hypothesis.strategies import sets, integers
@@ -29,7 +33,7 @@
default_settings = settings(default_settings, deadline=None)
-def good_numbits(numbits):
+def good_numbits(numbits: bytes) -> None:
"""Assert that numbits is good."""
# It shouldn't end with a zero byte, that should have been trimmed off.
assert (not numbits) or (numbits[-1] != 0)
@@ -42,7 +46,7 @@
@given(line_number_sets)
@settings(default_settings)
- def test_conversion(self, nums):
+ def test_conversion(self, nums: Iterable[int]) -> None:
numbits = nums_to_numbits(nums)
good_numbits(numbits)
nums2 = numbits_to_nums(numbits)
@@ -50,7 +54,7 @@
@given(line_number_sets, line_number_sets)
@settings(default_settings)
- def test_union(self, nums1, nums2):
+ def test_union(self, nums1: Set[int], nums2: Set[int]) -> None:
nb1 = nums_to_numbits(nums1)
good_numbits(nb1)
nb2 = nums_to_numbits(nums2)
@@ -62,7 +66,7 @@
@given(line_number_sets, line_number_sets)
@settings(default_settings)
- def test_intersection(self, nums1, nums2):
+ def test_intersection(self, nums1: Set[int], nums2: Set[int]) -> None:
nb1 = nums_to_numbits(nums1)
good_numbits(nb1)
nb2 = nums_to_numbits(nums2)
@@ -74,7 +78,7 @@
@given(line_number_sets, line_number_sets)
@settings(default_settings)
- def test_any_intersection(self, nums1, nums2):
+ def test_any_intersection(self, nums1: Set[int], nums2: Set[int]) -> None:
nb1 = nums_to_numbits(nums1)
good_numbits(nb1)
nb2 = nums_to_numbits(nums2)
@@ -86,7 +90,7 @@
@given(line_numbers, line_number_sets)
@settings(default_settings)
@example(152, {144})
- def test_num_in_numbits(self, num, nums):
+ def test_num_in_numbits(self, num: int, nums: Iterable[int]) -> None:
numbits = nums_to_numbits(nums)
good_numbits(numbits)
is_in = num_in_numbits(num, numbits)
@@ -98,7 +102,7 @@
run_in_temp_dir = False
- def setUp(self):
+ def setUp(self) -> None:
super().setUp()
conn = sqlite3.connect(":memory:")
register_sqlite_functions(conn)
@@ -113,7 +117,7 @@
)
self.addCleanup(self.cursor.close)
- def test_numbits_union(self):
+ def test_numbits_union(self) -> None:
res = self.cursor.execute(
"select numbits_union(" +
"(select numbits from data where id = 7)," +
@@ -127,7 +131,7 @@
answer = numbits_to_nums(list(res)[0][0])
assert expected == answer
- def test_numbits_intersection(self):
+ def test_numbits_intersection(self) -> None:
res = self.cursor.execute(
"select numbits_intersection(" +
"(select numbits from data where id = 7)," +
@@ -137,7 +141,7 @@
answer = numbits_to_nums(list(res)[0][0])
assert [63] == answer
- def test_numbits_any_intersection(self):
+ def test_numbits_any_intersection(self) -> None:
res = self.cursor.execute(
"select numbits_any_intersection(?, ?)",
(nums_to_numbits([1, 2, 3]), nums_to_numbits([3, 4, 5]))
@@ -152,11 +156,11 @@
answer = [any_inter for (any_inter,) in res]
assert [0] == answer
- def test_num_in_numbits(self):
+ def test_num_in_numbits(self) -> None:
res = self.cursor.execute("select id, num_in_numbits(12, numbits) from data order by id")
answer = [is_in for (id, is_in) in res]
assert [1, 1, 1, 1, 0, 1, 0, 0, 0, 0] == answer
- def test_numbits_to_nums(self):
+ def test_numbits_to_nums(self) -> None:
res = self.cursor.execute("select numbits_to_nums(?)", [nums_to_numbits([1, 2, 3])])
assert [1, 2, 3] == json.loads(res.fetchone()[0])
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_oddball.py python-coverage-7.2.7+dfsg1/tests/test_oddball.py
--- python-coverage-6.5.0+dfsg1/tests/test_oddball.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_oddball.py 2023-05-29 19:46:30.000000000 +0000
@@ -12,6 +12,7 @@
import coverage
from coverage import env
+from coverage.data import sorted_lines
from coverage.files import abs_file
from coverage.misc import import_local_file
@@ -23,7 +24,7 @@
class ThreadingTest(CoverageTest):
"""Tests of the threading support."""
- def test_threading(self):
+ def test_threading(self) -> None:
self.check_coverage("""\
import threading
@@ -43,7 +44,7 @@
""",
[1, 3, 4, 6, 7, 9, 10, 12, 13, 14, 15], "10")
- def test_thread_run(self):
+ def test_thread_run(self) -> None:
self.check_coverage("""\
import threading
@@ -66,7 +67,7 @@
class RecursionTest(CoverageTest):
"""Check what happens when recursive code gets near limits."""
- def test_short_recursion(self):
+ def test_short_recursion(self) -> None:
# We can definitely get close to 500 stack frames.
self.check_coverage("""\
def recur(n):
@@ -80,7 +81,7 @@
""",
[1, 2, 3, 5, 7, 8], "")
- def test_long_recursion(self):
+ def test_long_recursion(self) -> None:
# We can't finish a very deep recursion, but we don't crash.
with pytest.raises(RuntimeError):
with swallow_warnings("Trace function changed, data is likely wrong: None"):
@@ -96,7 +97,7 @@
[1, 2, 3, 5, 7], ""
)
- def test_long_recursion_recovery(self):
+ def test_long_recursion_recovery(self) -> None:
# Test the core of bug 93: https://github.com/nedbat/coveragepy/issues/93
# When recovering from a stack overflow, the Python trace function is
# disabled, but the C trace function is not. So if we're using a
@@ -123,6 +124,7 @@
with swallow_warnings("Trace function changed, data is likely wrong: None"):
self.start_import_stop(cov, "recur")
+ assert cov._collector is not None
pytrace = (cov._collector.tracer_name() == "PyTracer")
expected_missing = [3]
if pytrace: # pragma: no metacov
@@ -138,7 +140,7 @@
assert re.fullmatch(
r"Trace function changed, data is likely wrong: None != " +
r">",
+ ">",
cov._warnings[0],
)
else:
@@ -154,10 +156,9 @@
It may still fail occasionally, especially on PyPy.
"""
- @flaky
- @pytest.mark.skipif(env.JYTHON, reason="Don't bother on Jython")
+ @flaky # type: ignore[misc]
@pytest.mark.skipif(not env.C_TRACER, reason="Only the C tracer has refcounting issues")
- def test_for_leaks(self):
+ def test_for_leaks(self) -> None:
# Our original bad memory leak only happened on line numbers > 255, so
# make a code object with more lines than that. Ugly string mumbo
# jumbo to get 300 blank lines at the beginning..
@@ -204,11 +205,12 @@
"""Test that we properly manage the None refcount."""
@pytest.mark.skipif(not env.C_TRACER, reason="Only the C tracer has refcounting issues")
- def test_dropping_none(self): # pragma: not covered
+ def test_dropping_none(self) -> None: # pragma: not covered
# TODO: Mark this so it will only be run sometimes.
pytest.skip("This is too expensive for now (30s)")
# Start and stop coverage thousands of times to flush out bad
# reference counting, maybe.
+ _ = "this is just here to put a type comment on" # type: ignore[unreachable]
self.make_file("the_code.py", """\
import random
def f():
@@ -235,11 +237,10 @@
assert "Fatal" not in out
-@pytest.mark.skipif(env.JYTHON, reason="Pyexpat isn't a problem on Jython")
class PyexpatTest(CoverageTest):
"""Pyexpat screws up tracing. Make sure we've counter-defended properly."""
- def test_pyexpat(self):
+ def test_pyexpat(self) -> None:
# pyexpat calls the trace function explicitly (inexplicably), and does
# it wrong for exceptions. Parsing a DOCTYPE for some reason throws
# an exception internally, and triggers its wrong behavior. This test
@@ -291,7 +292,7 @@
in the trace function.
"""
- def test_exception(self):
+ def test_exception(self) -> None:
# Python 2.3's trace function doesn't get called with "return" if the
# scope is exiting due to an exception. This confounds our trace
# function which relies on scope announcements to track which files to
@@ -369,8 +370,8 @@
for callnames, lines_expected in runs:
# Make the list of functions we'll call for this test.
- callnames = callnames.split()
- calls = [getattr(sys.modules[cn], cn) for cn in callnames]
+ callnames_list = callnames.split()
+ calls = [getattr(sys.modules[cn], cn) for cn in callnames_list]
cov = coverage.Coverage()
cov.start()
@@ -383,17 +384,9 @@
# The file names are absolute, so keep just the base.
clean_lines = {}
data = cov.get_data()
- for callname in callnames:
+ for callname in callnames_list:
filename = callname + ".py"
- lines = data.lines(abs_file(filename))
- clean_lines[filename] = sorted(lines)
-
- if env.JYTHON: # pragma: only jython
- # Jython doesn't report on try or except lines, so take those
- # out of the expected lines.
- invisible = [202, 206, 302, 304]
- for lines in lines_expected.values():
- lines[:] = [l for l in lines if l not in invisible]
+ clean_lines[filename] = sorted_lines(data, abs_file(filename))
assert clean_lines == lines_expected
@@ -401,7 +394,7 @@
class DoctestTest(CoverageTest):
"""Tests invoked with doctest should measure properly."""
- def test_doctest(self):
+ def test_doctest(self) -> None:
# Doctests used to be traced, with their line numbers credited to the
# file they were in. Below, one of the doctests has four lines (1-4),
# which would incorrectly claim that lines 1-4 of the file were
@@ -436,13 +429,13 @@
self.start_import_stop(cov, "the_doctest")
data = cov.get_data()
assert len(data.measured_files()) == 1
- lines = data.lines(data.measured_files().pop())
+ lines = sorted_lines(data, data.measured_files().pop())
assert lines == [1, 3, 18, 19, 21, 23, 24]
class GettraceTest(CoverageTest):
"""Tests that we work properly with `sys.gettrace()`."""
- def test_round_trip_in_untraced_function(self):
+ def test_round_trip_in_untraced_function(self) -> None:
# https://github.com/nedbat/coveragepy/issues/575
self.make_file("main.py", """import sample""")
self.make_file("sample.py", """\
@@ -475,7 +468,7 @@
assert statements == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
assert missing == []
- def test_setting_new_trace_function(self):
+ def test_setting_new_trace_function(self) -> None:
# https://github.com/nedbat/coveragepy/issues/436
self.check_coverage('''\
import os.path
@@ -493,7 +486,7 @@
t = sys.gettrace()
assert t is tracer, t
- def test_unsets_trace():
+ def test_unsets_trace() -> None:
begin()
collect()
@@ -507,6 +500,7 @@
missing="5-7, 13-14",
)
+ assert self.last_module_name is not None
out = self.stdout().replace(self.last_module_name, "coverage_test")
expected = (
"call: coverage_test.py @ 12\n" +
@@ -518,7 +512,7 @@
@pytest.mark.expensive
@pytest.mark.skipif(env.METACOV, reason="Can't set trace functions during meta-coverage")
- def test_atexit_gettrace(self):
+ def test_atexit_gettrace(self) -> None:
# This is not a test of coverage at all, but of our understanding
# of this edge-case behavior in various Pythons.
@@ -550,7 +544,7 @@
class ExecTest(CoverageTest):
"""Tests of exec."""
- def test_correct_filename(self):
+ def test_correct_filename(self) -> None:
# https://github.com/nedbat/coveragepy/issues/380
# Bug was that exec'd files would have their lines attributed to the
# calling file. Make two files, both with ~30 lines, but no lines in
@@ -579,7 +573,7 @@
assert statements == [31]
assert missing == []
- def test_unencodable_filename(self):
+ def test_unencodable_filename(self) -> None:
# https://github.com/nedbat/coveragepy/issues/891
self.make_file("bug891.py", r"""exec(compile("pass", "\udcff.py", "exec"))""")
cov = coverage.Coverage()
@@ -596,7 +590,7 @@
https://github.com/nedbat/coveragepy/issues/416
"""
- def test_os_path_exists(self):
+ def test_os_path_exists(self) -> None:
# To see if this test still detects the problem, change isolate_module
# in misc.py to simply return its argument. It should fail with a
# StopIteration error.
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_parser.py python-coverage-7.2.7+dfsg1/tests/test_parser.py
--- python-coverage-6.5.0+dfsg1/tests/test_parser.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_parser.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,18 +3,23 @@
"""Tests for coverage.py's code parsing."""
+from __future__ import annotations
+
+import ast
import os.path
import textwrap
import warnings
+from typing import List
+
import pytest
from coverage import env
from coverage.exceptions import NotPython
-from coverage.parser import ast_dump, ast_parse, PythonParser
+from coverage.parser import ast_dump, PythonParser
from tests.coveragetest import CoverageTest, TESTS_DIR
-from tests.helpers import arcz_to_arcs, re_lines, xfail_pypy_3749
+from tests.helpers import arcz_to_arcs, re_lines, xfail_pypy38
class PythonParserTest(CoverageTest):
@@ -22,14 +27,14 @@
run_in_temp_dir = False
- def parse_source(self, text):
+ def parse_source(self, text: str) -> PythonParser:
"""Parse `text` as source, and return the `PythonParser` used."""
text = textwrap.dedent(text)
parser = PythonParser(text=text, exclude="nocover")
parser.parse_source()
return parser
- def test_exit_counts(self):
+ def test_exit_counts(self) -> None:
parser = self.parse_source("""\
# check some basic branch counting
class Foo:
@@ -46,7 +51,7 @@
2:1, 3:1, 4:2, 5:1, 7:1, 9:1, 10:1
}
- def test_generator_exit_counts(self):
+ def test_generator_exit_counts(self) -> None:
# https://github.com/nedbat/coveragepy/issues/324
parser = self.parse_source("""\
def gen(input):
@@ -62,7 +67,7 @@
5:1, # list -> exit
}
- def test_try_except(self):
+ def test_try_except(self) -> None:
parser = self.parse_source("""\
try:
a = 2
@@ -78,7 +83,7 @@
1: 1, 2:1, 3:2, 4:1, 5:2, 6:1, 7:1, 8:1, 9:1
}
- def test_excluded_classes(self):
+ def test_excluded_classes(self) -> None:
parser = self.parse_source("""\
class Foo:
def __init__(self):
@@ -92,7 +97,7 @@
1:0, 2:1, 3:1
}
- def test_missing_branch_to_excluded_code(self):
+ def test_missing_branch_to_excluded_code(self) -> None:
parser = self.parse_source("""\
if fooey:
a = 2
@@ -120,10 +125,10 @@
""")
assert parser.exit_counts() == { 1:1, 2:1, 3:1, 6:1 }
- def test_indentation_error(self):
+ def test_indentation_error(self) -> None:
msg = (
"Couldn't parse '' as Python source: " +
- "'unindent does not match any outer indentation level' at line 3"
+ "'unindent does not match any outer indentation level.*' at line 3"
)
with pytest.raises(NotPython, match=msg):
_ = self.parse_source("""\
@@ -132,15 +137,21 @@
1
""")
- def test_token_error(self):
- msg = "Couldn't parse '' as Python source: 'EOF in multi-line string' at line 1"
+ def test_token_error(self) -> None:
+ submsgs = [
+ r"EOF in multi-line string", # before 3.12.0b1
+ r"unterminated triple-quoted string literal .detected at line 1.", # after 3.12.0b1
+ ]
+ msg = (
+ r"Couldn't parse '' as Python source: '"
+ + r"(" + "|".join(submsgs) + ")"
+ + r"' at line 1"
+ )
with pytest.raises(NotPython, match=msg):
- _ = self.parse_source("""\
- '''
- """)
+ _ = self.parse_source("'''")
- @xfail_pypy_3749
- def test_decorator_pragmas(self):
+ @xfail_pypy38
+ def test_decorator_pragmas(self) -> None:
parser = self.parse_source("""\
# 1
@@ -175,8 +186,8 @@
assert parser.raw_statements == raw_statements
assert parser.statements == {8}
- @xfail_pypy_3749
- def test_decorator_pragmas_with_colons(self):
+ @xfail_pypy38
+ def test_decorator_pragmas_with_colons(self) -> None:
# A colon in a decorator expression would confuse the parser,
# ending the exclusion of the decorated function.
parser = self.parse_source("""\
@@ -196,7 +207,7 @@
assert parser.raw_statements == raw_statements
assert parser.statements == set()
- def test_class_decorator_pragmas(self):
+ def test_class_decorator_pragmas(self) -> None:
parser = self.parse_source("""\
class Foo(object):
def __init__(self):
@@ -210,8 +221,7 @@
assert parser.raw_statements == {1, 2, 3, 5, 6, 7, 8}
assert parser.statements == {1, 2, 3}
- @xfail_pypy_3749
- def test_empty_decorated_function(self):
+ def test_empty_decorated_function(self) -> None:
parser = self.parse_source("""\
def decorator(func):
return func
@@ -247,10 +257,13 @@
assert expected_arcs == parser.arcs()
assert expected_exits == parser.exit_counts()
- def test_fuzzed_double_parse(self):
+ def test_fuzzed_double_parse(self) -> None:
# https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=50381
# The second parse used to raise `TypeError: 'NoneType' object is not iterable`
- msg = "EOF in multi-line statement"
+ msg = (
+ r"(EOF in multi-line statement)" # before 3.12.0b1
+ + r"|(unmatched ']')" # after 3.12.0b1
+ )
with pytest.raises(NotPython, match=msg):
self.parse_source("]")
with pytest.raises(NotPython, match=msg):
@@ -262,13 +275,13 @@
run_in_temp_dir = False
- def parse_text(self, source):
+ def parse_text(self, source: str) -> PythonParser:
"""Parse Python source, and return the parser object."""
parser = PythonParser(text=textwrap.dedent(source))
parser.parse_source()
return parser
- def test_missing_arc_description(self):
+ def test_missing_arc_description(self) -> None:
# This code is never run, so the actual values don't matter.
parser = self.parse_text("""\
if x:
@@ -304,7 +317,7 @@
)
assert expected == parser.missing_arc_description(11, 13)
- def test_missing_arc_descriptions_for_small_callables(self):
+ def test_missing_arc_descriptions_for_small_callables(self) -> None:
parser = self.parse_text("""\
callables = [
lambda: 2,
@@ -318,12 +331,13 @@
assert expected == parser.missing_arc_description(2, -2)
expected = "line 3 didn't finish the generator expression on line 3"
assert expected == parser.missing_arc_description(3, -3)
- expected = "line 4 didn't finish the dictionary comprehension on line 4"
- assert expected == parser.missing_arc_description(4, -4)
- expected = "line 5 didn't finish the set comprehension on line 5"
- assert expected == parser.missing_arc_description(5, -5)
+ if env.PYBEHAVIOR.comprehensions_are_functions:
+ expected = "line 4 didn't finish the dictionary comprehension on line 4"
+ assert expected == parser.missing_arc_description(4, -4)
+ expected = "line 5 didn't finish the set comprehension on line 5"
+ assert expected == parser.missing_arc_description(5, -5)
- def test_missing_arc_descriptions_for_exceptions(self):
+ def test_missing_arc_descriptions_for_exceptions(self) -> None:
parser = self.parse_text("""\
try:
pass
@@ -343,7 +357,7 @@
)
assert expected == parser.missing_arc_description(5, 6)
- def test_missing_arc_descriptions_for_finally(self):
+ def test_missing_arc_descriptions_for_finally(self) -> None:
parser = self.parse_text("""\
def function():
for i in range(2):
@@ -417,7 +431,7 @@
)
assert expected == parser.missing_arc_description(18, -1)
- def test_missing_arc_descriptions_bug460(self):
+ def test_missing_arc_descriptions_bug460(self) -> None:
parser = self.parse_text("""\
x = 1
d = {
@@ -429,7 +443,7 @@
assert parser.missing_arc_description(2, -3) == "line 3 didn't finish the lambda on line 3"
@pytest.mark.skipif(not env.PYBEHAVIOR.match_case, reason="Match-case is new in 3.10")
- def test_match_case_with_default(self):
+ def test_match_case_with_default(self) -> None:
parser = self.parse_text("""\
for command in ["huh", "go home", "go n"]:
match command.split():
@@ -450,7 +464,7 @@
class ParserFileTest(CoverageTest):
"""Tests for coverage.py's code parsing from files."""
- def parse_file(self, filename):
+ def parse_file(self, filename: str) -> PythonParser:
"""Parse `text` as source, and return the `PythonParser` used."""
parser = PythonParser(filename=filename, exclude="nocover")
parser.parse_source()
@@ -459,7 +473,7 @@
@pytest.mark.parametrize("slug, newline", [
("unix", "\n"), ("dos", "\r\n"), ("mac", "\r"),
])
- def test_line_endings(self, slug, newline):
+ def test_line_endings(self, slug: str, newline: str) -> None:
text = """\
# check some basic branch counting
class Foo:
@@ -478,14 +492,14 @@
parser = self.parse_file(fname)
assert parser.exit_counts() == counts, f"Wrong for {fname!r}"
- def test_encoding(self):
+ def test_encoding(self) -> None:
self.make_file("encoded.py", """\
coverage = "\xe7\xf6v\xear\xe3g\xe9"
""")
parser = self.parse_file("encoded.py")
assert parser.exit_counts() == {1: 1}
- def test_missing_line_ending(self):
+ def test_missing_line_ending(self) -> None:
# Test that the set of statements is the same even if a final
# multi-line statement has no final newline.
# https://github.com/nedbat/coveragepy/issues/293
@@ -514,7 +528,7 @@
assert parser.statements == {1}
-def test_ast_dump():
+def test_ast_dump() -> None:
# Run the AST_DUMP code to make sure it doesn't fail, with some light
# assertions. Use parser.py as the test code since it is the longest file,
# and fitting, since it's the AST_DUMP code.
@@ -529,9 +543,9 @@
num_lines = len(source.splitlines())
with warnings.catch_warnings():
# stress_phystoken.tok has deprecation warnings, suppress them.
- warnings.filterwarnings("ignore", message=r".*invalid escape sequence",)
- ast_root = ast_parse(source)
- result = []
+ warnings.filterwarnings("ignore", message=r".*invalid escape sequence")
+ ast_root = ast.parse(source)
+ result: List[str] = []
ast_dump(ast_root, print=result.append)
if num_lines < 100:
continue
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_phystokens.py python-coverage-7.2.7+dfsg1/tests/test_phystokens.py
--- python-coverage-6.5.0+dfsg1/tests/test_phystokens.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_phystokens.py 2023-05-29 19:46:30.000000000 +0000
@@ -12,7 +12,6 @@
from coverage import env
from coverage.phystokens import source_token_lines, source_encoding
-from coverage.phystokens import neuter_encoding_declaration, compile_unicode
from coverage.python import get_python_source
from tests.coveragetest import CoverageTest, TESTS_DIR
@@ -33,7 +32,7 @@
('ws', ' '), ('num', '2'), ('op', ')')],
]
-# Mixed-whitespace program, and its token stream.
+# Mixed-white-space program, and its token stream.
MIXED_WS = """\
def hello():
a="Hello world!"
@@ -59,7 +58,7 @@
run_in_temp_dir = False
- def check_tokenization(self, source):
+ def check_tokenization(self, source: str) -> None:
"""Tokenize `source`, then put it back together, should be the same."""
tokenized = ""
for line in source_token_lines(source):
@@ -72,26 +71,26 @@
tokenized = re.sub(r"(?m)[ \t]+$", "", tokenized)
assert source == tokenized
- def check_file_tokenization(self, fname):
+ def check_file_tokenization(self, fname: str) -> None:
"""Use the contents of `fname` for `check_tokenization`."""
self.check_tokenization(get_python_source(fname))
- def test_simple(self):
+ def test_simple(self) -> None:
assert list(source_token_lines(SIMPLE)) == SIMPLE_TOKENS
self.check_tokenization(SIMPLE)
- def test_missing_final_newline(self):
+ def test_missing_final_newline(self) -> None:
# We can tokenize source that is missing the final newline.
assert list(source_token_lines(SIMPLE.rstrip())) == SIMPLE_TOKENS
- def test_tab_indentation(self):
+ def test_tab_indentation(self) -> None:
# Mixed tabs and spaces...
assert list(source_token_lines(MIXED_WS)) == MIXED_WS_TOKENS
- def test_bug_822(self):
+ def test_bug_822(self) -> None:
self.check_tokenization(BUG_822)
- def test_tokenize_real_file(self):
+ def test_tokenize_real_file(self) -> None:
# Check the tokenization of a real file (large, btw).
real_file = os.path.join(TESTS_DIR, "test_coverage.py")
self.check_file_tokenization(real_file)
@@ -100,11 +99,11 @@
"stress_phystoken.tok",
"stress_phystoken_dos.tok",
])
- def test_stress(self, fname):
+ def test_stress(self, fname: str) -> None:
# Check the tokenization of the stress-test files.
# And check that those files haven't been incorrectly "fixed".
with warnings.catch_warnings():
- warnings.filterwarnings("ignore", message=r".*invalid escape sequence",)
+ warnings.filterwarnings("ignore", message=r".*invalid escape sequence")
stress = os.path.join(TESTS_DIR, fname)
self.check_file_tokenization(stress)
@@ -117,7 +116,7 @@
run_in_temp_dir = False
- def test_soft_keywords(self):
+ def test_soft_keywords(self) -> None:
source = textwrap.dedent("""\
match re.match(something):
case ["what"]:
@@ -147,7 +146,7 @@
assert tokens[11][3] == ("nam", "case")
-# The default encoding is different in Python 2 and Python 3.
+# The default source file encoding.
DEF_ENCODING = "utf-8"
@@ -169,152 +168,40 @@
run_in_temp_dir = False
- def test_detect_source_encoding(self):
+ def test_detect_source_encoding(self) -> None:
for _, source, expected in ENCODING_DECLARATION_SOURCES:
assert source_encoding(source) == expected, f"Wrong encoding in {source!r}"
- def test_detect_source_encoding_not_in_comment(self):
+ def test_detect_source_encoding_not_in_comment(self) -> None:
# Should not detect anything here
source = b'def parse(src, encoding=None):\n pass'
assert source_encoding(source) == DEF_ENCODING
- def test_dont_detect_source_encoding_on_third_line(self):
+ def test_dont_detect_source_encoding_on_third_line(self) -> None:
# A coding declaration doesn't count on the third line.
source = b"\n\n# coding=cp850\n\n"
assert source_encoding(source) == DEF_ENCODING
- def test_detect_source_encoding_of_empty_file(self):
+ def test_detect_source_encoding_of_empty_file(self) -> None:
# An important edge case.
assert source_encoding(b"") == DEF_ENCODING
- def test_bom(self):
+ def test_bom(self) -> None:
# A BOM means utf-8.
source = b"\xEF\xBB\xBFtext = 'hello'\n"
assert source_encoding(source) == 'utf-8-sig'
- def test_bom_with_encoding(self):
+ def test_bom_with_encoding(self) -> None:
source = b"\xEF\xBB\xBF# coding: utf-8\ntext = 'hello'\n"
assert source_encoding(source) == 'utf-8-sig'
- def test_bom_is_wrong(self):
+ def test_bom_is_wrong(self) -> None:
# A BOM with an explicit non-utf8 encoding is an error.
source = b"\xEF\xBB\xBF# coding: cp850\n"
with pytest.raises(SyntaxError, match="encoding problem: utf-8"):
source_encoding(source)
- def test_unknown_encoding(self):
+ def test_unknown_encoding(self) -> None:
source = b"# coding: klingon\n"
with pytest.raises(SyntaxError, match="unknown encoding: klingon"):
source_encoding(source)
-
-
-class NeuterEncodingDeclarationTest(CoverageTest):
- """Tests of phystokens.neuter_encoding_declaration()."""
-
- run_in_temp_dir = False
-
- def test_neuter_encoding_declaration(self):
- for lines_diff_expected, source, _ in ENCODING_DECLARATION_SOURCES:
- neutered = neuter_encoding_declaration(source.decode("ascii"))
- neutered = neutered.encode("ascii")
-
- # The neutered source should have the same number of lines.
- source_lines = source.splitlines()
- neutered_lines = neutered.splitlines()
- assert len(source_lines) == len(neutered_lines)
-
- # Only one of the lines should be different.
- lines_different = sum(
- int(nline != sline) for nline, sline in zip(neutered_lines, source_lines)
- )
- assert lines_diff_expected == lines_different
-
- # The neutered source will be detected as having no encoding
- # declaration.
- assert source_encoding(neutered) == DEF_ENCODING, f"Wrong encoding in {neutered!r}"
-
- def test_two_encoding_declarations(self):
- input_src = textwrap.dedent("""\
- # -*- coding: ascii -*-
- # -*- coding: utf-8 -*-
- # -*- coding: utf-16 -*-
- """)
- expected_src = textwrap.dedent("""\
- # (deleted declaration) -*-
- # (deleted declaration) -*-
- # -*- coding: utf-16 -*-
- """)
- output_src = neuter_encoding_declaration(input_src)
- assert expected_src == output_src
-
- def test_one_encoding_declaration(self):
- input_src = textwrap.dedent("""\
- # -*- coding: utf-16 -*-
- # Just a comment.
- # -*- coding: ascii -*-
- """)
- expected_src = textwrap.dedent("""\
- # (deleted declaration) -*-
- # Just a comment.
- # -*- coding: ascii -*-
- """)
- output_src = neuter_encoding_declaration(input_src)
- assert expected_src == output_src
-
-
-class Bug529Test(CoverageTest):
- """Test of bug 529"""
-
- def test_bug_529(self):
- # Don't over-neuter coding declarations. This happened with a test
- # file which contained code in multi-line strings, all with coding
- # declarations. The neutering of the file also changed the multi-line
- # strings, which it shouldn't have.
- self.make_file("the_test.py", '''\
- # -*- coding: utf-8 -*-
- import unittest
- class Bug529Test(unittest.TestCase):
- def test_two_strings_are_equal(self):
- src1 = u"""\\
- # -*- coding: utf-8 -*-
- # Just a comment.
- """
- src2 = u"""\\
- # -*- coding: utf-8 -*-
- # Just a comment.
- """
- self.assertEqual(src1, src2)
-
- if __name__ == "__main__":
- unittest.main()
- ''')
- status, out = self.run_command_status("coverage run the_test.py")
- assert status == 0
- assert "OK" in out
- # If this test fails, the output will be super-confusing, because it
- # has a failing unit test contained within the failing unit test.
-
-
-class CompileUnicodeTest(CoverageTest):
- """Tests of compiling Unicode strings."""
-
- run_in_temp_dir = False
-
- def assert_compile_unicode(self, source):
- """Assert that `source` will compile properly with `compile_unicode`."""
- source += "a = 42\n"
- # This doesn't raise an exception:
- code = compile_unicode(source, "", "exec")
- globs = {}
- exec(code, globs)
- assert globs['a'] == 42
-
- def test_cp1252(self):
- uni = """# coding: cp1252\n# \u201C curly \u201D\n"""
- self.assert_compile_unicode(uni)
-
- def test_double_coding_declaration(self):
- # Build this string in a weird way so that actual vim's won't try to
- # interpret it...
- uni = "# -*- coding:utf-8 -*-\n# v" + "im: fileencoding=utf-8\n"
- self.assert_compile_unicode(uni)
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_plugins.py python-coverage-7.2.7+dfsg1/tests/test_plugins.py
--- python-coverage-6.5.0+dfsg1/tests/test_plugins.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_plugins.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,20 +3,25 @@
"""Tests for plugins."""
+from __future__ import annotations
+
import inspect
import io
import math
import os.path
+
+from typing import Any, Dict, List, Optional
from xml.etree import ElementTree
import pytest
import coverage
-from coverage import env
+from coverage import Coverage, env
from coverage.control import Plugins
-from coverage.data import line_counts
+from coverage.data import line_counts, sorted_lines
from coverage.exceptions import CoverageWarning, NoSource, PluginError
from coverage.misc import import_local_file
+from coverage.types import TConfigSectionOut, TLineNo, TPluginConfig
import coverage.plugin
@@ -24,18 +29,24 @@
from tests.helpers import CheckUniqueFilenames, swallow_warnings
-class FakeConfig:
+class NullConfig(TPluginConfig):
+ """A plugin configure thing when we don't really need one."""
+ def get_plugin_options(self, plugin: str) -> TConfigSectionOut:
+ return {}
+
+
+class FakeConfig(TPluginConfig):
"""A fake config for use in tests."""
- def __init__(self, plugin, options):
+ def __init__(self, plugin: str, options: Dict[str, Any]) -> None:
self.plugin = plugin
self.options = options
- self.asked_for = []
+ self.asked_for: List[str] = []
- def get_plugin_options(self, module):
- """Just return the options for `module` if this is the right module."""
- self.asked_for.append(module)
- if module == self.plugin:
+ def get_plugin_options(self, plugin: str) -> TConfigSectionOut:
+ """Just return the options for `plugin` if this is the right module."""
+ self.asked_for.append(plugin)
+ if plugin == self.plugin:
return self.options
else:
return {}
@@ -44,7 +55,7 @@
class LoadPluginsTest(CoverageTest):
"""Test Plugins.load_plugins directly."""
- def test_implicit_boolean(self):
+ def test_implicit_boolean(self) -> None:
self.make_file("plugin1.py", """\
from coverage import CoveragePlugin
@@ -62,7 +73,7 @@
plugins = Plugins.load_plugins(["plugin1"], config)
assert plugins
- def test_importing_and_configuring(self):
+ def test_importing_and_configuring(self) -> None:
self.make_file("plugin1.py", """\
from coverage import CoveragePlugin
@@ -79,11 +90,11 @@
plugins = list(Plugins.load_plugins(["plugin1"], config))
assert len(plugins) == 1
- assert plugins[0].this_is == "me"
- assert plugins[0].options == {'a': 'hello'}
+ assert plugins[0].this_is == "me" # type: ignore
+ assert plugins[0].options == {'a': 'hello'} # type: ignore
assert config.asked_for == ['plugin1']
- def test_importing_and_configuring_more_than_one(self):
+ def test_importing_and_configuring_more_than_one(self) -> None:
self.make_file("plugin1.py", """\
from coverage import CoveragePlugin
@@ -110,9 +121,9 @@
plugins = list(Plugins.load_plugins(["plugin1", "plugin2"], config))
assert len(plugins) == 2
- assert plugins[0].this_is == "me"
- assert plugins[0].options == {'a': 'hello'}
- assert plugins[1].options == {}
+ assert plugins[0].this_is == "me" # type: ignore
+ assert plugins[0].options == {'a': 'hello'} # type: ignore
+ assert plugins[1].options == {} # type: ignore
assert config.asked_for == ['plugin1', 'plugin2']
# The order matters...
@@ -120,28 +131,28 @@
plugins = list(Plugins.load_plugins(["plugin2", "plugin1"], config))
assert len(plugins) == 2
- assert plugins[0].options == {}
- assert plugins[1].this_is == "me"
- assert plugins[1].options == {'a': 'second'}
+ assert plugins[0].options == {} # type: ignore
+ assert plugins[1].this_is == "me" # type: ignore
+ assert plugins[1].options == {'a': 'second'} # type: ignore
- def test_cant_import(self):
+ def test_cant_import(self) -> None:
with pytest.raises(ImportError, match="No module named '?plugin_not_there'?"):
- _ = Plugins.load_plugins(["plugin_not_there"], None)
+ _ = Plugins.load_plugins(["plugin_not_there"], NullConfig())
- def test_plugin_must_define_coverage_init(self):
+ def test_plugin_must_define_coverage_init(self) -> None:
self.make_file("no_plugin.py", """\
from coverage import CoveragePlugin
Nothing = 0
""")
msg_pat = "Plugin module 'no_plugin' didn't define a coverage_init function"
with pytest.raises(PluginError, match=msg_pat):
- list(Plugins.load_plugins(["no_plugin"], None))
+ list(Plugins.load_plugins(["no_plugin"], NullConfig()))
class PluginTest(CoverageTest):
"""Test plugins through the Coverage class."""
- def test_plugin_imported(self):
+ def test_plugin_imported(self) -> None:
# Prove that a plugin will be imported.
self.make_file("my_plugin.py", """\
from coverage import CoveragePlugin
@@ -162,7 +173,7 @@
with open("evidence.out") as f:
assert f.read() == "we are here!"
- def test_missing_plugin_raises_import_error(self):
+ def test_missing_plugin_raises_import_error(self) -> None:
# Prove that a missing plugin will raise an ImportError.
with pytest.raises(ImportError, match="No module named '?does_not_exist_woijwoicweo'?"):
cov = coverage.Coverage()
@@ -170,7 +181,7 @@
cov.start()
cov.stop()
- def test_bad_plugin_isnt_hidden(self):
+ def test_bad_plugin_isnt_hidden(self) -> None:
# Prove that a plugin with an error in it will raise the error.
self.make_file("plugin_over_zero.py", "1/0")
with pytest.raises(ZeroDivisionError):
@@ -179,7 +190,7 @@
cov.start()
cov.stop()
- def test_plugin_sys_info(self):
+ def test_plugin_sys_info(self) -> None:
self.make_file("plugin_sys_info.py", """\
import coverage
@@ -213,7 +224,7 @@
]
assert expected_end == out_lines[-len(expected_end):]
- def test_plugin_with_no_sys_info(self):
+ def test_plugin_with_no_sys_info(self) -> None:
self.make_file("plugin_no_sys_info.py", """\
import coverage
@@ -239,7 +250,7 @@
]
assert expected_end == out_lines[-len(expected_end):]
- def test_local_files_are_importable(self):
+ def test_local_files_are_importable(self) -> None:
self.make_file("importing_plugin.py", """\
from coverage import CoveragePlugin
import local_module
@@ -264,7 +275,7 @@
@pytest.mark.skipif(env.C_TRACER, reason="This test is only about PyTracer.")
class PluginWarningOnPyTracerTest(CoverageTest):
"""Test that we get a controlled exception with plugins on PyTracer."""
- def test_exception_if_plugins_on_pytracer(self):
+ def test_exception_if_plugins_on_pytracer(self) -> None:
self.make_file("simple.py", "a = 1")
cov = coverage.Coverage()
@@ -285,7 +296,7 @@
class GoodFileTracerTest(FileTracerTest):
"""Tests of file tracer plugin happy paths."""
- def test_plugin1(self):
+ def test_plugin1(self) -> None:
self.make_file("simple.py", """\
import try_xyz
a = 1
@@ -311,7 +322,7 @@
_, statements, _, _ = cov.analysis(zzfile)
assert statements == [105, 106, 107, 205, 206, 207]
- def make_render_and_caller(self):
+ def make_render_and_caller(self) -> None:
"""Make the render.py and caller.py files we need."""
# plugin2 emulates a dynamic tracing plugin: the caller's locals
# are examined to determine the source file and line number.
@@ -343,21 +354,18 @@
# quux_5.html will be omitted from the results.
assert render("quux_5.html", 3) == "[quux_5.html @ 3]"
-
- # For Python 2, make sure unicode is working.
- assert render(u"uni_3.html", 2) == "[uni_3.html @ 2]"
""")
# will try to read the actual source files, so make some
# source files.
- def lines(n):
+ def lines(n: int) -> str:
"""Make a string with n lines of text."""
return "".join("line %d\n" % i for i in range(n))
self.make_file("bar_4.html", lines(4))
self.make_file("foo_7.html", lines(7))
- def test_plugin2(self):
+ def test_plugin2(self) -> None:
self.make_render_and_caller()
cov = coverage.Coverage(omit=["*quux*"])
@@ -382,12 +390,7 @@
assert "quux_5.html" not in line_counts(cov.get_data())
- _, statements, missing, _ = cov.analysis("uni_3.html")
- assert statements == [1, 2, 3]
- assert missing == [1]
- assert "uni_3.html" in line_counts(cov.get_data())
-
- def test_plugin2_with_branch(self):
+ def test_plugin2_with_branch(self) -> None:
self.make_render_and_caller()
cov = coverage.Coverage(branch=True, omit=["*quux*"])
@@ -408,7 +411,7 @@
assert analysis.missing == {1, 2, 3, 6, 7}
- def test_plugin2_with_text_report(self):
+ def test_plugin2_with_text_report(self) -> None:
self.make_render_and_caller()
cov = coverage.Coverage(branch=True, omit=["*quux*"])
@@ -430,7 +433,7 @@
assert expected == report
assert math.isclose(total, 4 / 11 * 100)
- def test_plugin2_with_html_report(self):
+ def test_plugin2_with_html_report(self) -> None:
self.make_render_and_caller()
cov = coverage.Coverage(branch=True, omit=["*quux*"])
@@ -445,7 +448,7 @@
self.assert_exists("htmlcov/bar_4_html.html")
self.assert_exists("htmlcov/foo_7_html.html")
- def test_plugin2_with_xml_report(self):
+ def test_plugin2_with_xml_report(self) -> None:
self.make_render_and_caller()
cov = coverage.Coverage(branch=True, omit=["*quux*"])
@@ -476,7 +479,7 @@
'name': 'foo_7.html',
}
- def test_defer_to_python(self):
+ def test_defer_to_python(self) -> None:
# A plugin that measures, but then wants built-in python reporting.
self.make_file("fairly_odd_plugin.py", """\
# A plugin that claims all the odd lines are executed, and none of
@@ -529,7 +532,7 @@
assert expected == report
assert total == 50
- def test_find_unexecuted(self):
+ def test_find_unexecuted(self) -> None:
self.make_file("unexecuted_plugin.py", """\
import os
import coverage.plugin
@@ -585,7 +588,7 @@
class BadFileTracerTest(FileTracerTest):
"""Test error handling around file tracer plugins."""
- def run_plugin(self, module_name):
+ def run_plugin(self, module_name: str) -> Coverage:
"""Run a plugin with the given module_name.
Uses a few fixed Python files.
@@ -617,7 +620,14 @@
cov.save() # pytest-cov does a save after stop, so we'll do it too.
return cov
- def run_bad_plugin(self, module_name, plugin_name, our_error=True, excmsg=None, excmsgs=None):
+ def run_bad_plugin(
+ self,
+ module_name: str,
+ plugin_name: str,
+ our_error: bool = True,
+ excmsg: Optional[str] = None,
+ excmsgs: Optional[List[str]] = None,
+ ) -> None:
"""Run a file, and see that the plugin failed.
`module_name` and `plugin_name` is the module and name of the plugin to
@@ -639,7 +649,7 @@
self.run_plugin(module_name)
stderr = self.stderr()
- stderr += "".join(w.message.args[0] for w in warns)
+ stderr += "".join(str(w.message) for w in warns)
if our_error:
# The exception we're causing should only appear once.
assert stderr.count("# Oh noes!") == 1
@@ -650,9 +660,9 @@
# or:
# Disabling plug-in '...' due to an exception:
print([str(w) for w in warns.list])
- warns = [w for w in warns.list if issubclass(w.category, CoverageWarning)]
- assert len(warns) == 1
- warnmsg = warns[0].message.args[0]
+ warnings = [w for w in warns.list if issubclass(w.category, CoverageWarning)]
+ assert len(warnings) == 1
+ warnmsg = str(warnings[0].message)
assert f"Disabling plug-in '{module_name}.{plugin_name}' due to " in warnmsg
if excmsg:
@@ -661,7 +671,7 @@
found_exc = any(em in stderr for em in excmsgs) # pragma: part covered
assert found_exc, f"expected one of {excmsgs} in stderr"
- def test_file_tracer_has_no_file_tracer_method(self):
+ def test_file_tracer_has_no_file_tracer_method(self) -> None:
self.make_file("bad_plugin.py", """\
class Plugin(object):
pass
@@ -671,7 +681,7 @@
""")
self.run_bad_plugin("bad_plugin", "Plugin", our_error=False)
- def test_file_tracer_has_inherited_sourcefilename_method(self):
+ def test_file_tracer_has_inherited_sourcefilename_method(self) -> None:
self.make_file("bad_plugin.py", """\
import coverage
class Plugin(coverage.CoveragePlugin):
@@ -690,7 +700,7 @@
excmsg="Class 'bad_plugin.FileTracer' needs to implement source_filename()",
)
- def test_plugin_has_inherited_filereporter_method(self):
+ def test_plugin_has_inherited_filereporter_method(self) -> None:
self.make_file("bad_plugin.py", """\
import coverage
class Plugin(coverage.CoveragePlugin):
@@ -710,7 +720,7 @@
with pytest.raises(NotImplementedError, match=expected_msg):
cov.report()
- def test_file_tracer_fails(self):
+ def test_file_tracer_fails(self) -> None:
self.make_file("bad_plugin.py", """\
import coverage.plugin
class Plugin(coverage.plugin.CoveragePlugin):
@@ -722,7 +732,7 @@
""")
self.run_bad_plugin("bad_plugin", "Plugin")
- def test_file_tracer_fails_eventually(self):
+ def test_file_tracer_fails_eventually(self) -> None:
# Django coverage plugin can report on a few files and then fail.
# https://github.com/nedbat/coveragepy/issues/1011
self.make_file("bad_plugin.py", """\
@@ -753,7 +763,7 @@
""")
self.run_bad_plugin("bad_plugin", "Plugin")
- def test_file_tracer_returns_wrong(self):
+ def test_file_tracer_returns_wrong(self) -> None:
self.make_file("bad_plugin.py", """\
import coverage.plugin
class Plugin(coverage.plugin.CoveragePlugin):
@@ -767,7 +777,7 @@
"bad_plugin", "Plugin", our_error=False, excmsg="'float' object has no attribute",
)
- def test_has_dynamic_source_filename_fails(self):
+ def test_has_dynamic_source_filename_fails(self) -> None:
self.make_file("bad_plugin.py", """\
import coverage.plugin
class Plugin(coverage.plugin.CoveragePlugin):
@@ -783,7 +793,7 @@
""")
self.run_bad_plugin("bad_plugin", "Plugin")
- def test_source_filename_fails(self):
+ def test_source_filename_fails(self) -> None:
self.make_file("bad_plugin.py", """\
import coverage.plugin
class Plugin(coverage.plugin.CoveragePlugin):
@@ -799,7 +809,7 @@
""")
self.run_bad_plugin("bad_plugin", "Plugin")
- def test_source_filename_returns_wrong(self):
+ def test_source_filename_returns_wrong(self) -> None:
self.make_file("bad_plugin.py", """\
import coverage.plugin
class Plugin(coverage.plugin.CoveragePlugin):
@@ -823,7 +833,7 @@
],
)
- def test_dynamic_source_filename_fails(self):
+ def test_dynamic_source_filename_fails(self) -> None:
self.make_file("bad_plugin.py", """\
import coverage.plugin
class Plugin(coverage.plugin.CoveragePlugin):
@@ -842,7 +852,7 @@
""")
self.run_bad_plugin("bad_plugin", "Plugin")
- def test_line_number_range_raises_error(self):
+ def test_line_number_range_raises_error(self) -> None:
self.make_file("bad_plugin.py", """\
import coverage.plugin
class Plugin(coverage.plugin.CoveragePlugin):
@@ -864,7 +874,7 @@
"bad_plugin", "Plugin", our_error=False, excmsg="borked!",
)
- def test_line_number_range_returns_non_tuple(self):
+ def test_line_number_range_returns_non_tuple(self) -> None:
self.make_file("bad_plugin.py", """\
import coverage.plugin
class Plugin(coverage.plugin.CoveragePlugin):
@@ -886,7 +896,7 @@
"bad_plugin", "Plugin", our_error=False, excmsg="line_number_range must return 2-tuple",
)
- def test_line_number_range_returns_triple(self):
+ def test_line_number_range_returns_triple(self) -> None:
self.make_file("bad_plugin.py", """\
import coverage.plugin
class Plugin(coverage.plugin.CoveragePlugin):
@@ -908,7 +918,7 @@
"bad_plugin", "Plugin", our_error=False, excmsg="line_number_range must return 2-tuple",
)
- def test_line_number_range_returns_pair_of_strings(self):
+ def test_line_number_range_returns_pair_of_strings(self) -> None:
self.make_file("bad_plugin.py", """\
import coverage.plugin
class Plugin(coverage.plugin.CoveragePlugin):
@@ -940,12 +950,13 @@
run_in_temp_dir = False
- def test_configurer_plugin(self):
+ def test_configurer_plugin(self) -> None:
cov = coverage.Coverage()
cov.set_option("run:plugins", ["tests.plugin_config"])
cov.start()
cov.stop() # pragma: nested
excluded = cov.get_option("report:exclude_lines")
+ assert isinstance(excluded, list)
assert "pragma: custom" in excluded
assert "pragma: or whatever" in excluded
@@ -953,7 +964,7 @@
class DynamicContextPluginTest(CoverageTest):
"""Tests of plugins that implement `dynamic_context`."""
- def make_plugin_capitalized_testnames(self, filename):
+ def make_plugin_capitalized_testnames(self, filename: str) -> None:
"""Create a dynamic context plugin that capitalizes the part after 'test_'."""
self.make_file(filename, """\
from coverage import CoveragePlugin
@@ -970,7 +981,7 @@
reg.add_dynamic_context(Plugin())
""")
- def make_plugin_track_render(self, filename):
+ def make_plugin_track_render(self, filename: str) -> None:
"""Make a dynamic context plugin that tracks 'render_' functions."""
self.make_file(filename, """\
from coverage import CoveragePlugin
@@ -986,7 +997,7 @@
reg.add_dynamic_context(Plugin())
""")
- def make_test_files(self):
+ def make_test_files(self) -> None:
"""Make some files to use while testing dynamic context plugins."""
self.make_file("rendering.py", """\
def html_tag(tag, content):
@@ -1005,7 +1016,7 @@
self.make_file("testsuite.py", """\
import rendering
- def test_html_tag():
+ def test_html_tag() -> None:
assert rendering.html_tag('b', 'hello') == 'hello'
def doctest_html_tag():
@@ -1013,7 +1024,7 @@
rendering.html_tag('i', 'text') == 'text'
'''.strip())
- def test_renderers():
+ def test_renderers() -> None:
assert rendering.render_paragraph('hello') == 'hello
'
assert rendering.render_bold('wide') == 'wide'
assert rendering.render_span('world') == 'world'
@@ -1025,7 +1036,7 @@
return html
""")
- def run_all_functions(self, cov, suite_name): # pragma: nested
+ def run_all_functions(self, cov: Coverage, suite_name: str) -> None: # pragma: nested
"""Run all functions in `suite_name` under coverage."""
cov.start()
suite = import_local_file(suite_name)
@@ -1038,7 +1049,7 @@
finally:
cov.stop()
- def test_plugin_standalone(self):
+ def test_plugin_standalone(self) -> None:
self.make_plugin_capitalized_testnames('plugin_tests.py')
self.make_test_files()
@@ -1055,13 +1066,13 @@
expected = ['', 'doctest:HTML_TAG', 'test:HTML_TAG', 'test:RENDERERS']
assert expected == sorted(data.measured_contexts())
data.set_query_context("doctest:HTML_TAG")
- assert [2] == data.lines(filenames['rendering.py'])
+ assert [2] == sorted_lines(data, filenames['rendering.py'])
data.set_query_context("test:HTML_TAG")
- assert [2] == data.lines(filenames['rendering.py'])
+ assert [2] == sorted_lines(data, filenames['rendering.py'])
data.set_query_context("test:RENDERERS")
- assert [2, 5, 8, 11] == sorted(data.lines(filenames['rendering.py']))
+ assert [2, 5, 8, 11] == sorted_lines(data, filenames['rendering.py'])
- def test_static_context(self):
+ def test_static_context(self) -> None:
self.make_plugin_capitalized_testnames('plugin_tests.py')
self.make_test_files()
@@ -1082,7 +1093,7 @@
]
assert expected == sorted(data.measured_contexts())
- def test_plugin_with_test_function(self):
+ def test_plugin_with_test_function(self) -> None:
self.make_plugin_capitalized_testnames('plugin_tests.py')
self.make_test_files()
@@ -1107,15 +1118,15 @@
]
assert expected == sorted(data.measured_contexts())
- def assert_context_lines(context, lines):
+ def assert_context_lines(context: str, lines: List[TLineNo]) -> None:
data.set_query_context(context)
- assert lines == sorted(data.lines(filenames['rendering.py']))
+ assert lines == sorted_lines(data, filenames['rendering.py'])
assert_context_lines("doctest:HTML_TAG", [2])
assert_context_lines("testsuite.test_html_tag", [2])
assert_context_lines("testsuite.test_renderers", [2, 5, 8, 11])
- def test_multiple_plugins(self):
+ def test_multiple_plugins(self) -> None:
self.make_plugin_capitalized_testnames('plugin_tests.py')
self.make_plugin_track_render('plugin_renderers.py')
self.make_test_files()
@@ -1145,9 +1156,9 @@
]
assert expected == sorted(data.measured_contexts())
- def assert_context_lines(context, lines):
+ def assert_context_lines(context: str, lines: List[TLineNo]) -> None:
data.set_query_context(context)
- assert lines == sorted(data.lines(filenames['rendering.py']))
+ assert lines == sorted_lines(data, filenames['rendering.py'])
assert_context_lines("test:HTML_TAG", [2])
assert_context_lines("test:RENDERERS", [2, 5, 8, 11])
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_process.py python-coverage-7.2.7+dfsg1/tests/test_process.py
--- python-coverage-6.5.0+dfsg1/tests/test_process.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_process.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,14 +3,19 @@
"""Tests for process behavior of coverage.py."""
+from __future__ import annotations
+
import glob
import os
import os.path
+import platform
import re
import stat
import sys
import textwrap
+from typing import Any
+
import pytest
import coverage
@@ -25,7 +30,7 @@
class ProcessTest(CoverageTest):
"""Tests of the per-process behavior of coverage.py."""
- def test_save_on_exit(self):
+ def test_save_on_exit(self) -> None:
self.make_file("mycode.py", """\
h = "Hello"
w = "world"
@@ -35,7 +40,7 @@
self.run_command("coverage run mycode.py")
self.assert_exists(".coverage")
- def test_tests_dir_is_importable(self):
+ def test_tests_dir_is_importable(self) -> None:
# Checks that we can import modules from the tests directory at all!
self.make_file("mycode.py", """\
import covmod1
@@ -49,7 +54,7 @@
self.assert_exists(".coverage")
assert out == 'done\n'
- def test_coverage_run_envvar_is_in_coveragerun(self):
+ def test_coverage_run_envvar_is_in_coveragerun(self) -> None:
# Test that we are setting COVERAGE_RUN when we run.
self.make_file("envornot.py", """\
import os
@@ -64,7 +69,7 @@
out = self.run_command("coverage run envornot.py")
assert out == "true\n"
- def make_b_or_c_py(self):
+ def make_b_or_c_py(self) -> None:
"""Create b_or_c.py, used in a few of these tests."""
# "b_or_c.py b" will run 6 lines.
# "b_or_c.py c" will run 7 lines.
@@ -81,7 +86,7 @@
print('done')
""")
- def test_append_data(self):
+ def test_append_data(self) -> None:
self.make_b_or_c_py()
out = self.run_command("coverage run b_or_c.py b")
@@ -100,7 +105,7 @@
data.read()
assert line_counts(data)['b_or_c.py'] == 8
- def test_append_data_with_different_file(self):
+ def test_append_data_with_different_file(self) -> None:
self.make_b_or_c_py()
self.make_file(".coveragerc", """\
@@ -124,7 +129,7 @@
data.read()
assert line_counts(data)['b_or_c.py'] == 8
- def test_append_can_create_a_data_file(self):
+ def test_append_can_create_a_data_file(self) -> None:
self.make_b_or_c_py()
out = self.run_command("coverage run --append b_or_c.py b")
@@ -138,7 +143,7 @@
data.read()
assert line_counts(data)['b_or_c.py'] == 6
- def test_combine_with_rc(self):
+ def test_combine_with_rc(self) -> None:
self.make_b_or_c_py()
self.make_file(".coveragerc", """\
@@ -182,7 +187,7 @@
TOTAL 8 0 100%
""")
- def test_combine_with_aliases(self):
+ def test_combine_with_aliases(self) -> None:
self.make_file("d1/x.py", """\
a = 1
b = 2
@@ -217,6 +222,8 @@
self.assert_file_count(".coverage.*", 2)
+ self.make_file("src/x.py", "")
+
self.run_command("coverage combine")
self.assert_exists(".coverage")
@@ -234,7 +241,7 @@
assert expected == actual
assert list(summary.values())[0] == 6
- def test_erase_parallel(self):
+ def test_erase_parallel(self) -> None:
self.make_file(".coveragerc", """\
[run]
data_file = data.dat
@@ -251,7 +258,7 @@
self.assert_doesnt_exist("data.dat.gooey")
self.assert_exists(".coverage")
- def test_missing_source_file(self):
+ def test_missing_source_file(self) -> None:
# Check what happens if the source is missing when reporting happens.
self.make_file("fleeting.py", """\
s = 'goodbye, cruel world!'
@@ -276,14 +283,14 @@
assert "Traceback" not in out
assert status == 1
- def test_running_missing_file(self):
+ def test_running_missing_file(self) -> None:
status, out = self.run_command_status("coverage run xyzzy.py")
assert re.search("No file to run: .*xyzzy.py", out)
assert "raceback" not in out
assert "rror" not in out
assert status == 1
- def test_code_throws(self):
+ def test_code_throws(self) -> None:
self.make_file("throw.py", """\
class MyException(Exception):
pass
@@ -313,7 +320,7 @@
assert 'raise MyException("hey!")' in out
assert status == 1
- def test_code_exits(self):
+ def test_code_exits(self) -> None:
self.make_file("exit.py", """\
import sys
def f1():
@@ -335,7 +342,7 @@
assert status == status2
assert status == 17
- def test_code_exits_no_arg(self):
+ def test_code_exits_no_arg(self) -> None:
self.make_file("exit_none.py", """\
import sys
def f1():
@@ -352,7 +359,7 @@
assert status == 0
@pytest.mark.skipif(not hasattr(os, "fork"), reason="Can't test os.fork, it doesn't exist.")
- def test_fork(self):
+ def test_fork(self) -> None:
self.make_file("fork.py", """\
import os
@@ -395,7 +402,7 @@
data.read()
assert line_counts(data)['fork.py'] == 9
- def test_warnings_during_reporting(self):
+ def test_warnings_during_reporting(self) -> None:
# While fixing issue #224, the warnings were being printed far too
# often. Make sure they're not any more.
self.make_file("hello.py", """\
@@ -416,7 +423,7 @@
out = self.run_command("coverage html")
assert out.count("Module xyzzy was never imported.") == 0
- def test_warns_if_never_run(self):
+ def test_warns_if_never_run(self) -> None:
# Note: the name of the function can't have "warning" in it, or the
# absolute path of the file will have "warning" in it, and an assertion
# will fail.
@@ -435,7 +442,7 @@
assert "Exception" not in out
@pytest.mark.skipif(env.METACOV, reason="Can't test tracers changing during metacoverage")
- def test_warnings_trace_function_changed_with_threads(self):
+ def test_warnings_trace_function_changed_with_threads(self) -> None:
# https://github.com/nedbat/coveragepy/issues/164
self.make_file("bug164.py", """\
@@ -455,7 +462,7 @@
assert "Hello\n" in out
assert "warning" not in out
- def test_warning_trace_function_changed(self):
+ def test_warning_trace_function_changed(self) -> None:
self.make_file("settrace.py", """\
import sys
print("Hello")
@@ -471,7 +478,7 @@
# When meta-coverage testing, this test doesn't work, because it finds
# coverage.py's own trace function.
@pytest.mark.skipif(env.METACOV, reason="Can't test timid during coverage measurement.")
- def test_timid(self):
+ def test_timid(self) -> None:
# Test that the --timid command line argument properly swaps the tracer
# function for a simpler one.
#
@@ -525,7 +532,7 @@
timid_out = self.run_command("coverage run --timid showtrace.py")
assert timid_out == "PyTracer\n"
- def test_warn_preimported(self):
+ def test_warn_preimported(self) -> None:
self.make_file("hello.py", """\
import goodbye
import coverage
@@ -552,7 +559,7 @@
@pytest.mark.expensive
@pytest.mark.skipif(not env.C_TRACER, reason="fullcoverage only works with the C tracer.")
@pytest.mark.skipif(env.METACOV, reason="Can't test fullcoverage when measuring ourselves")
- def test_fullcoverage(self):
+ def test_fullcoverage(self) -> None:
# fullcoverage is a trick to get stdlib modules measured from
# the very beginning of the process. Here we import os and
# then check how many lines are measured.
@@ -576,9 +583,7 @@
# Pypy passes locally, but fails in CI? Perhaps the version of macOS is
# significant? https://foss.heptapod.net/pypy/pypy/-/issues/3074
@pytest.mark.skipif(env.PYPY, reason="PyPy is unreliable with this test")
- # Jython as of 2.7.1rc3 won't compile a filename that isn't utf-8.
- @pytest.mark.skipif(env.JYTHON, reason="Jython can't handle this test")
- def test_lang_c(self):
+ def test_lang_c(self) -> None:
# LANG=C forces getfilesystemencoding on Linux to 'ascii', which causes
# failures with non-ascii file names. We don't want to make a real file
# with strange characters, though, because that gets the test runners
@@ -595,7 +600,7 @@
out = self.run_command("coverage run weird_file.py")
assert out == "1\n2\n"
- def test_deprecation_warnings(self):
+ def test_deprecation_warnings(self) -> None:
# Test that coverage doesn't trigger deprecation warnings.
# https://github.com/nedbat/coveragepy/issues/305
self.make_file("allok.py", """\
@@ -612,7 +617,7 @@
out = self.run_command("python allok.py")
assert out == "No warnings!\n"
- def test_run_twice(self):
+ def test_run_twice(self) -> None:
# https://github.com/nedbat/coveragepy/issues/353
self.make_file("foo.py", """\
def foo():
@@ -643,7 +648,7 @@
)
assert expected == out
- def test_module_name(self):
+ def test_module_name(self) -> None:
# https://github.com/nedbat/coveragepy/issues/478
# Make sure help doesn't show a silly command name when run as a
# module, like it used to:
@@ -658,7 +663,7 @@
class EnvironmentTest(CoverageTest):
"""Tests using try_execfile.py to test the execution environment."""
- def assert_tryexecfile_output(self, expected, actual):
+ def assert_tryexecfile_output(self, expected: str, actual: str) -> None:
"""Assert that the output we got is a successful run of try_execfile.py.
`expected` and `actual` must be the same, modulo a few slight known
@@ -667,35 +672,29 @@
"""
# First, is this even credible try_execfile.py output?
assert '"DATA": "xyzzy"' in actual
-
- if env.JYTHON: # pragma: only jython
- # Argv0 is different for Jython, remove that from the comparison.
- expected = re_lines_text(r'\s+"argv0":', expected, match=False)
- actual = re_lines_text(r'\s+"argv0":', actual, match=False)
-
assert actual == expected
- def test_coverage_run_is_like_python(self):
+ def test_coverage_run_is_like_python(self) -> None:
with open(TRY_EXECFILE) as f:
self.make_file("run_me.py", f.read())
expected = self.run_command("python run_me.py")
actual = self.run_command("coverage run run_me.py")
self.assert_tryexecfile_output(expected, actual)
- def test_coverage_run_far_away_is_like_python(self):
+ def test_coverage_run_far_away_is_like_python(self) -> None:
with open(TRY_EXECFILE) as f:
self.make_file("sub/overthere/prog.py", f.read())
expected = self.run_command("python sub/overthere/prog.py")
actual = self.run_command("coverage run sub/overthere/prog.py")
self.assert_tryexecfile_output(expected, actual)
- def test_coverage_run_dashm_is_like_python_dashm(self):
+ def test_coverage_run_dashm_is_like_python_dashm(self) -> None:
# These -m commands assume the coverage tree is on the path.
expected = self.run_command("python -m process_test.try_execfile")
actual = self.run_command("coverage run -m process_test.try_execfile")
self.assert_tryexecfile_output(expected, actual)
- def test_coverage_run_dir_is_like_python_dir(self):
+ def test_coverage_run_dir_is_like_python_dir(self) -> None:
with open(TRY_EXECFILE) as f:
self.make_file("with_main/__main__.py", f.read())
@@ -703,7 +702,7 @@
actual = self.run_command("coverage run with_main")
self.assert_tryexecfile_output(expected, actual)
- def test_coverage_run_dashm_dir_no_init_is_like_python(self):
+ def test_coverage_run_dashm_dir_no_init_is_like_python(self) -> None:
with open(TRY_EXECFILE) as f:
self.make_file("with_main/__main__.py", f.read())
@@ -711,7 +710,7 @@
actual = self.run_command("coverage run -m with_main")
self.assert_tryexecfile_output(expected, actual)
- def test_coverage_run_dashm_dir_with_init_is_like_python(self):
+ def test_coverage_run_dashm_dir_with_init_is_like_python(self) -> None:
with open(TRY_EXECFILE) as f:
self.make_file("with_main/__main__.py", f.read())
self.make_file("with_main/__init__.py", "")
@@ -720,7 +719,7 @@
actual = self.run_command("coverage run -m with_main")
self.assert_tryexecfile_output(expected, actual)
- def test_coverage_run_dashm_equal_to_doubledashsource(self):
+ def test_coverage_run_dashm_equal_to_doubledashsource(self) -> None:
"""regression test for #328
When imported by -m, a module's __name__ is __main__, but we need the
@@ -733,7 +732,7 @@
)
self.assert_tryexecfile_output(expected, actual)
- def test_coverage_run_dashm_superset_of_doubledashsource(self):
+ def test_coverage_run_dashm_superset_of_doubledashsource(self) -> None:
"""Edge case: --source foo -m foo.bar"""
# Ugh: without this config file, we'll get a warning about
# CoverageWarning: Module process_test was previously imported,
@@ -757,7 +756,7 @@
assert st == 0
assert self.line_count(out) == 6, out
- def test_coverage_run_script_imports_doubledashsource(self):
+ def test_coverage_run_script_imports_doubledashsource(self) -> None:
# This file imports try_execfile, which compiles it to .pyc, so the
# first run will have __file__ == "try_execfile.py" and the second will
# have __file__ == "try_execfile.pyc", which throws off the comparison.
@@ -776,7 +775,7 @@
assert st == 0
assert self.line_count(out) == 6, out
- def test_coverage_run_dashm_is_like_python_dashm_off_path(self):
+ def test_coverage_run_dashm_is_like_python_dashm_off_path(self) -> None:
# https://github.com/nedbat/coveragepy/issues/242
self.make_file("sub/__init__.py", "")
with open(TRY_EXECFILE) as f:
@@ -786,7 +785,7 @@
actual = self.run_command("coverage run -m sub.run_me")
self.assert_tryexecfile_output(expected, actual)
- def test_coverage_run_dashm_is_like_python_dashm_with__main__207(self):
+ def test_coverage_run_dashm_is_like_python_dashm_with__main__207(self) -> None:
# https://github.com/nedbat/coveragepy/issues/207
self.make_file("package/__init__.py", "print('init')")
self.make_file("package/__main__.py", "print('main')")
@@ -794,7 +793,7 @@
actual = self.run_command("coverage run -m package")
assert expected == actual
- def test_coverage_zip_is_like_python(self):
+ def test_coverage_zip_is_like_python(self) -> None:
# Test running coverage from a zip file itself. Some environments
# (windows?) zip up the coverage main to be used as the coverage
# command.
@@ -805,7 +804,7 @@
actual = self.run_command(f"python {cov_main} run run_me.py")
self.assert_tryexecfile_output(expected, actual)
- def test_coverage_custom_script(self):
+ def test_coverage_custom_script(self) -> None:
# https://github.com/nedbat/coveragepy/issues/678
# If sys.path[0] isn't the Python default, then coverage.py won't
# fiddle with it.
@@ -839,7 +838,13 @@
assert "hello-xyzzy" in out
@pytest.mark.skipif(env.WINDOWS, reason="Windows can't make symlinks")
- def test_bug_862(self):
+ @pytest.mark.skipif(
+ platform.python_version().endswith("+"),
+ reason="setuptools barfs on dev versions: https://github.com/pypa/packaging/issues/678"
+ # https://github.com/nedbat/coveragepy/issues/1556
+ # TODO: get rid of pkg_resources
+ )
+ def test_bug_862(self) -> None:
# This simulates how pyenv and pyenv-virtualenv end up creating the
# coverage executable.
self.make_file("elsewhere/bin/fake-coverage", """\
@@ -854,7 +859,7 @@
out = self.run_command("somewhere/bin/fake-coverage run bar.py")
assert "inside foo\n" == out
- def test_bug_909(self):
+ def test_bug_909(self) -> None:
# https://github.com/nedbat/coveragepy/issues/909
# The __init__ files were being imported before measurement started,
# so the line in __init__.py was being marked as missed, and there were
@@ -888,7 +893,7 @@
# TODO: do we need these as process tests if we have test_execfile.py:RunFileTest?
- def test_excepthook(self):
+ def test_excepthook(self) -> None:
self.make_file("excepthook.py", """\
import sys
@@ -904,10 +909,8 @@
""")
cov_st, cov_out = self.run_command_status("coverage run excepthook.py")
py_st, py_out = self.run_command_status("python excepthook.py")
- if not env.JYTHON:
- assert cov_st == py_st
- assert cov_st == 1
-
+ assert cov_st == py_st
+ assert cov_st == 1
assert "in excepthook" in py_out
assert cov_out == py_out
@@ -920,7 +923,7 @@
@pytest.mark.skipif(not env.CPYTHON,
reason="non-CPython handles excepthook exits differently, punt for now."
)
- def test_excepthook_exit(self):
+ def test_excepthook_exit(self) -> None:
self.make_file("excepthook_exit.py", """\
import sys
@@ -941,7 +944,7 @@
assert cov_out == py_out
@pytest.mark.skipif(env.PYPY, reason="PyPy handles excepthook throws differently.")
- def test_excepthook_throw(self):
+ def test_excepthook_throw(self) -> None:
self.make_file("excepthook_throw.py", """\
import sys
@@ -958,34 +961,31 @@
""")
cov_st, cov_out = self.run_command_status("coverage run excepthook_throw.py")
py_st, py_out = self.run_command_status("python excepthook_throw.py")
- if not env.JYTHON:
- assert cov_st == py_st
- assert cov_st == 1
-
+ assert cov_st == py_st
+ assert cov_st == 1
assert "in excepthook" in py_out
assert cov_out == py_out
-@pytest.mark.skipif(env.JYTHON, reason="Coverage command names don't work on Jython")
class AliasedCommandTest(CoverageTest):
"""Tests of the version-specific command aliases."""
run_in_temp_dir = False
- def test_major_version_works(self):
+ def test_major_version_works(self) -> None:
# "coverage3" works on py3
cmd = "coverage%d" % sys.version_info[0]
out = self.run_command(cmd)
assert "Code coverage for Python" in out
- def test_wrong_alias_doesnt_work(self):
+ def test_wrong_alias_doesnt_work(self) -> None:
# "coverage2" doesn't work on py3
assert sys.version_info[0] in [2, 3] # Let us know when Python 4 is out...
badcmd = "coverage%d" % (5 - sys.version_info[0])
out = self.run_command(badcmd)
assert "Code coverage for Python" not in out
- def test_specific_alias_works(self):
+ def test_specific_alias_works(self) -> None:
# "coverage-3.9" works on py3.9
cmd = "coverage-%d.%d" % sys.version_info[:2]
out = self.run_command(cmd)
@@ -996,7 +996,7 @@
"coverage%d" % sys.version_info[0],
"coverage-%d.%d" % sys.version_info[:2],
])
- def test_aliases_used_in_messages(self, cmd):
+ def test_aliases_used_in_messages(self, cmd: str) -> None:
out = self.run_command(f"{cmd} foobar")
assert "Unknown command: 'foobar'" in out
assert f"Use '{cmd} help' for help" in out
@@ -1007,7 +1007,7 @@
run_in_temp_dir = False
- def assert_pydoc_ok(self, name, thing):
+ def assert_pydoc_ok(self, name: str, thing: Any) -> None:
"""Check that pydoc of `name` finds the docstring from `thing`."""
# Run pydoc.
out = self.run_command("python -m pydoc " + name)
@@ -1019,17 +1019,17 @@
for line in thing.__doc__.splitlines():
assert line.strip() in out
- def test_pydoc_coverage(self):
+ def test_pydoc_coverage(self) -> None:
self.assert_pydoc_ok("coverage", coverage)
- def test_pydoc_coverage_coverage(self):
+ def test_pydoc_coverage_coverage(self) -> None:
self.assert_pydoc_ok("coverage.Coverage", coverage.Coverage)
class FailUnderTest(CoverageTest):
"""Tests of the --fail-under switch."""
- def setUp(self):
+ def setUp(self) -> None:
super().setUp()
self.make_file("forty_two_plus.py", """\
# I have 42.857% (3/7) coverage!
@@ -1043,25 +1043,25 @@
""")
self.make_data_file(lines={abs_file("forty_two_plus.py"): [2, 3, 4]})
- def test_report_43_is_ok(self):
+ def test_report_43_is_ok(self) -> None:
st, out = self.run_command_status("coverage report --fail-under=43")
assert st == 0
assert self.last_line_squeezed(out) == "TOTAL 7 4 43%"
- def test_report_43_is_not_ok(self):
+ def test_report_43_is_not_ok(self) -> None:
st, out = self.run_command_status("coverage report --fail-under=44")
assert st == 2
expected = "Coverage failure: total of 43 is less than fail-under=44"
assert expected == self.last_line_squeezed(out)
- def test_report_42p86_is_not_ok(self):
+ def test_report_42p86_is_not_ok(self) -> None:
self.make_file(".coveragerc", "[report]\nprecision = 2")
st, out = self.run_command_status("coverage report --fail-under=42.88")
assert st == 2
expected = "Coverage failure: total of 42.86 is less than fail-under=42.88"
assert expected == self.last_line_squeezed(out)
- def test_report_99p9_is_not_ok(self):
+ def test_report_99p9_is_not_ok(self) -> None:
# A file with 99.9% coverage:
self.make_file("ninety_nine_plus.py",
"a = 1\n" +
@@ -1078,7 +1078,7 @@
class FailUnderNoFilesTest(CoverageTest):
"""Test that nothing to report results in an error exit status."""
- def test_report(self):
+ def test_report(self) -> None:
self.make_file(".coveragerc", "[report]\nfail_under = 99\n")
st, out = self.run_command_status("coverage report")
assert 'No data to report.' in out
@@ -1087,13 +1087,14 @@
class FailUnderEmptyFilesTest(CoverageTest):
"""Test that empty files produce the proper fail_under exit status."""
- def test_report(self):
+ def test_report(self) -> None:
self.make_file(".coveragerc", "[report]\nfail_under = 99\n")
self.make_file("empty.py", "")
st, _ = self.run_command_status("coverage run empty.py")
assert st == 0
st, _ = self.run_command_status("coverage report")
- assert st == 2
+ # An empty file is marked as 100% covered, so this is ok.
+ assert st == 0
@pytest.mark.skipif(env.WINDOWS, reason="Windows can't delete the directory in use.")
@@ -1111,12 +1112,12 @@
print(sys.argv[1])
"""
- def test_removing_directory(self):
+ def test_removing_directory(self) -> None:
self.make_file("bug806.py", self.BUG_806)
out = self.run_command("coverage run bug806.py noerror")
assert out == "noerror\n"
- def test_removing_directory_with_error(self):
+ def test_removing_directory_with_error(self) -> None:
self.make_file("bug806.py", self.BUG_806)
out = self.run_command("coverage run bug806.py")
path = python_reported_file('bug806.py')
@@ -1135,7 +1136,7 @@
class ProcessStartupTest(CoverageTest):
"""Test that we can measure coverage in sub-processes."""
- def setUp(self):
+ def setUp(self) -> None:
super().setUp()
# Main will run sub.py
@@ -1151,7 +1152,7 @@
f.close()
""")
- def test_subprocess_with_pth_files(self):
+ def test_subprocess_with_pth_files(self) -> None:
# An existing data file should not be read when a subprocess gets
# measured automatically. Create the data file here with bogus data in
# it.
@@ -1175,7 +1176,7 @@
data.read()
assert line_counts(data)['sub.py'] == 3
- def test_subprocess_with_pth_files_and_parallel(self):
+ def test_subprocess_with_pth_files_and_parallel(self) -> None:
# https://github.com/nedbat/coveragepy/issues/492
self.make_file("coverage.ini", """\
[run]
@@ -1222,7 +1223,7 @@
@pytest.mark.parametrize("dashm", ["-m", ""])
@pytest.mark.parametrize("package", ["pkg", ""])
@pytest.mark.parametrize("source", ["main", "sub"])
- def test_pth_and_source_work_together(self, dashm, package, source):
+ def test_pth_and_source_work_together(self, dashm: str, package: str, source: str) -> None:
"""Run the test for a particular combination of factors.
The arguments are all strings:
@@ -1237,14 +1238,14 @@
``--source`` argument.
"""
- def fullname(modname):
+ def fullname(modname: str) -> str:
"""What is the full module name for `modname` for this test?"""
if package and dashm:
return '.'.join((package, modname))
else:
return modname
- def path(basename):
+ def path(basename: str) -> str:
"""Where should `basename` be created for this test?"""
return os.path.join(package, basename)
@@ -1258,7 +1259,6 @@
self.make_file(path("__init__.py"), "")
# sub.py will write a few lines.
self.make_file(path("sub.py"), """\
- # Avoid 'with' so Jython can play along.
f = open("out.txt", "w")
f.write("Hello, world!")
f.close()
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_python.py python-coverage-7.2.7+dfsg1/tests/test_python.py
--- python-coverage-6.5.0+dfsg1/tests/test_python.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_python.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,9 @@
"""Tests of coverage/python.py"""
+from __future__ import annotations
+
+import pathlib
import sys
import pytest
@@ -23,13 +26,14 @@
"encoding",
["utf-8", "gb2312", "hebrew", "shift_jis", "cp1252"],
)
- def test_get_encoded_zip_files(self, encoding):
+ def test_get_encoded_zip_files(self, encoding: str) -> None:
# See igor.py, do_zipmods, for the text of these files.
zip_file = "tests/zipmods.zip"
sys.path.append(zip_file) # So we can import the files.
filename = zip_file + "/encoded_" + encoding + ".py"
filename = os_sep(filename)
zip_data = get_zip_bytes(filename)
+ assert zip_data is not None
zip_text = zip_data.decode(encoding)
assert 'All OK' in zip_text
# Run the code to see that we really got it encoded properly.
@@ -37,9 +41,8 @@
assert mod.encoding == encoding
-def test_source_for_file(tmpdir):
- path = tmpdir.join("a.py")
- src = str(path)
+def test_source_for_file(tmp_path: pathlib.Path) -> None:
+ src = str(tmp_path / "a.py")
assert source_for_file(src) == src
assert source_for_file(src + 'c') == src
assert source_for_file(src + 'o') == src
@@ -48,18 +51,15 @@
@pytest.mark.skipif(not env.WINDOWS, reason="not windows")
-def test_source_for_file_windows(tmpdir):
- path = tmpdir.join("a.py")
- src = str(path)
+def test_source_for_file_windows(tmp_path: pathlib.Path) -> None:
+ a_py = tmp_path / "a.py"
+ src = str(a_py)
# On windows if a pyw exists, it is an acceptable source
- path_windows = tmpdir.ensure("a.pyw")
+ path_windows = tmp_path / "a.pyw"
+ path_windows.write_text("")
assert str(path_windows) == source_for_file(src + 'c')
# If both pyw and py exist, py is preferred
- path.ensure(file=True)
+ a_py.write_text("")
assert source_for_file(src + 'c') == src
-
-
-def test_source_for_file_jython():
- assert source_for_file("a$py.class") == "a.py"
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_report_common.py python-coverage-7.2.7+dfsg1/tests/test_report_common.py
--- python-coverage-6.5.0+dfsg1/tests/test_report_common.py 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_report_common.py 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +1,283 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+"""Tests of behavior common to all reporting."""
+
+from __future__ import annotations
+
+import textwrap
+
+import coverage
+from coverage import env
+from coverage.files import abs_file
+
+from tests.coveragetest import CoverageTest
+from tests.goldtest import contains, doesnt_contain
+from tests.helpers import arcz_to_arcs, os_sep
+
+
+class ReportMapsPathsTest(CoverageTest):
+ """Check that reporting implicitly maps paths."""
+
+ def make_files(self, data: str, settings: bool = False) -> None:
+ """Create the test files we need for line coverage."""
+ src = """\
+ if VER == 1:
+ print("line 2")
+ if VER == 2:
+ print("line 4")
+ if VER == 3:
+ print("line 6")
+ """
+ self.make_file("src/program.py", src)
+ self.make_file("ver1/program.py", src)
+ self.make_file("ver2/program.py", src)
+
+ if data == "line":
+ self.make_data_file(
+ lines={
+ abs_file("ver1/program.py"): [1, 2, 3, 5],
+ abs_file("ver2/program.py"): [1, 3, 4, 5],
+ }
+ )
+ else:
+ self.make_data_file(
+ arcs={
+ abs_file("ver1/program.py"): arcz_to_arcs(".1 12 23 35 5."),
+ abs_file("ver2/program.py"): arcz_to_arcs(".1 13 34 45 5."),
+ }
+ )
+
+ if settings:
+ self.make_file(".coveragerc", """\
+ [paths]
+ source =
+ src
+ ver1
+ ver2
+ """)
+
+ def test_map_paths_during_line_report_without_setting(self) -> None:
+ self.make_files(data="line")
+ cov = coverage.Coverage()
+ cov.load()
+ cov.report(show_missing=True)
+ expected = textwrap.dedent(os_sep("""\
+ Name Stmts Miss Cover Missing
+ -----------------------------------------------
+ ver1/program.py 6 2 67% 4, 6
+ ver2/program.py 6 2 67% 2, 6
+ -----------------------------------------------
+ TOTAL 12 4 67%
+ """))
+ assert expected == self.stdout()
+
+ def test_map_paths_during_line_report(self) -> None:
+ self.make_files(data="line", settings=True)
+ cov = coverage.Coverage()
+ cov.load()
+ cov.report(show_missing=True)
+ expected = textwrap.dedent(os_sep("""\
+ Name Stmts Miss Cover Missing
+ ----------------------------------------------
+ src/program.py 6 1 83% 6
+ ----------------------------------------------
+ TOTAL 6 1 83%
+ """))
+ assert expected == self.stdout()
+
+ def test_map_paths_during_branch_report_without_setting(self) -> None:
+ self.make_files(data="arcs")
+ cov = coverage.Coverage(branch=True)
+ cov.load()
+ cov.report(show_missing=True)
+ expected = textwrap.dedent(os_sep("""\
+ Name Stmts Miss Branch BrPart Cover Missing
+ -------------------------------------------------------------
+ ver1/program.py 6 2 6 3 58% 1->3, 4, 6
+ ver2/program.py 6 2 6 3 58% 2, 3->5, 6
+ -------------------------------------------------------------
+ TOTAL 12 4 12 6 58%
+ """))
+ assert expected == self.stdout()
+
+ def test_map_paths_during_branch_report(self) -> None:
+ self.make_files(data="arcs", settings=True)
+ cov = coverage.Coverage(branch=True)
+ cov.load()
+ cov.report(show_missing=True)
+ expected = textwrap.dedent(os_sep("""\
+ Name Stmts Miss Branch BrPart Cover Missing
+ ------------------------------------------------------------
+ src/program.py 6 1 6 1 83% 6
+ ------------------------------------------------------------
+ TOTAL 6 1 6 1 83%
+ """))
+ assert expected == self.stdout()
+
+ def test_map_paths_during_annotate(self) -> None:
+ self.make_files(data="line", settings=True)
+ cov = coverage.Coverage()
+ cov.load()
+ cov.annotate()
+ self.assert_exists(os_sep("src/program.py,cover"))
+ self.assert_doesnt_exist(os_sep("ver1/program.py,cover"))
+ self.assert_doesnt_exist(os_sep("ver2/program.py,cover"))
+
+ def test_map_paths_during_html_report(self) -> None:
+ self.make_files(data="line", settings=True)
+ cov = coverage.Coverage()
+ cov.load()
+ cov.html_report()
+ contains("htmlcov/index.html", os_sep("src/program.py"))
+ doesnt_contain("htmlcov/index.html", os_sep("ver1/program.py"), os_sep("ver2/program.py"))
+
+ def test_map_paths_during_xml_report(self) -> None:
+ self.make_files(data="line", settings=True)
+ cov = coverage.Coverage()
+ cov.load()
+ cov.xml_report()
+ contains("coverage.xml", "src/program.py")
+ doesnt_contain("coverage.xml", "ver1/program.py", "ver2/program.py")
+
+ def test_map_paths_during_json_report(self) -> None:
+ self.make_files(data="line", settings=True)
+ cov = coverage.Coverage()
+ cov.load()
+ cov.json_report()
+ def os_sepj(s: str) -> str:
+ return os_sep(s).replace("\\", r"\\")
+ contains("coverage.json", os_sepj("src/program.py"))
+ doesnt_contain("coverage.json", os_sepj("ver1/program.py"), os_sepj("ver2/program.py"))
+
+ def test_map_paths_during_lcov_report(self) -> None:
+ self.make_files(data="line", settings=True)
+ cov = coverage.Coverage()
+ cov.load()
+ cov.lcov_report()
+ contains("coverage.lcov", os_sep("src/program.py"))
+ doesnt_contain("coverage.lcov", os_sep("ver1/program.py"), os_sep("ver2/program.py"))
+
+
+class ReportWithJinjaTest(CoverageTest):
+ """Tests of Jinja-like behavior.
+
+ Jinja2 compiles a template into Python code, and then runs the Python code
+ to render the template. But during rendering, it uses the template name
+ (for example, "template.j2") as the file name, not the Python code file
+ name. Then during reporting, we will try to parse template.j2 as Python
+ code.
+
+ If the file can be parsed, it's included in the report (as a Python file!).
+ If it can't be parsed, then it's not included in the report.
+
+ These tests confirm that code doesn't raise an exception (as reported in
+ #1553), and that the current (incorrect) behavior remains stable. Ideally,
+ good.j2 wouldn't be listed at all, since we can't report on it accurately.
+
+ See https://github.com/nedbat/coveragepy/issues/1553 for more detail, and
+ https://github.com/nedbat/coveragepy/issues/1623 for an issue about this
+ behavior.
+
+ """
+
+ def make_files(self) -> None:
+ """Create test files: two Jinja templates, and data from rendering them."""
+ # A Jinja2 file that is syntactically acceptable Python (though it wont run).
+ self.make_file("good.j2", """\
+ {{ data }}
+ line2
+ line3
+ """)
+ # A Jinja2 file that is a Python syntax error.
+ self.make_file("bad.j2", """\
+ This is data: {{ data }}.
+ line 2
+ line 3
+ """)
+ self.make_data_file(
+ lines={
+ abs_file("good.j2"): [1, 3, 5, 7, 9],
+ abs_file("bad.j2"): [1, 3, 5, 7, 9],
+ }
+ )
+
+ def test_report(self) -> None:
+ self.make_files()
+ cov = coverage.Coverage()
+ cov.load()
+ cov.report(show_missing=True)
+ expected = textwrap.dedent("""\
+ Name Stmts Miss Cover Missing
+ ---------------------------------------
+ good.j2 3 1 67% 2
+ ---------------------------------------
+ TOTAL 3 1 67%
+ """)
+ assert expected == self.stdout()
+
+ def test_html(self) -> None:
+ self.make_files()
+ cov = coverage.Coverage()
+ cov.load()
+ cov.html_report()
+ contains("htmlcov/index.html", """\
+
+
+ good.j2
+ 3
+ 1
+ 0
+ 67%
+
+ """
+ )
+ doesnt_contain("htmlcov/index.html", "bad.j2")
+
+ def test_xml(self) -> None:
+ self.make_files()
+ cov = coverage.Coverage()
+ cov.load()
+ cov.xml_report()
+ contains("coverage.xml", 'filename="good.j2"')
+ if env.PYVERSION >= (3, 8): # Py3.7 puts attributes in the other order.
+ contains("coverage.xml",
+ ' ',
+ ' ',
+ ' ',
+ )
+ doesnt_contain("coverage.xml", 'filename="bad.j2"')
+ if env.PYVERSION >= (3, 8): # Py3.7 puts attributes in the other order.
+ doesnt_contain("coverage.xml", ' None:
+ self.make_files()
+ cov = coverage.Coverage()
+ cov.load()
+ cov.json_report()
+ contains("coverage.json",
+ # Notice the .json report claims lines in good.j2 executed that
+ # don't even exist in good.j2...
+ '"files": {"good.j2": {"executed_lines": [1, 3, 5, 7, 9], ' +
+ '"summary": {"covered_lines": 2, "num_statements": 3',
+ )
+ doesnt_contain("coverage.json", "bad.j2")
+
+ def test_lcov(self) -> None:
+ self.make_files()
+ cov = coverage.Coverage()
+ cov.load()
+ cov.lcov_report()
+ with open("coverage.lcov") as lcov:
+ actual = lcov.read()
+ expected = textwrap.dedent("""\
+ TN:
+ SF:good.j2
+ DA:1,1,FHs1rDakj9p/NAzMCu3Kgw
+ DA:3,1,DGOyp8LEgI+3CcdFYw9uKQ
+ DA:2,0,5iUbzxp9w7peeTPjJbvmBQ
+ LF:3
+ LH:2
+ end_of_record
+ """)
+ assert expected == actual
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_report_core.py python-coverage-7.2.7+dfsg1/tests/test_report_core.py
--- python-coverage-6.5.0+dfsg1/tests/test_report_core.py 1970-01-01 00:00:00.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_report_core.py 2023-05-29 19:46:30.000000000 +0000
@@ -0,0 +1,68 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+"""Tests for helpers in report.py"""
+
+from __future__ import annotations
+
+from typing import IO, Iterable, List, Optional, Type
+
+import pytest
+
+from coverage.exceptions import CoverageException
+from coverage.report_core import render_report
+from coverage.types import TMorf
+
+from tests.coveragetest import CoverageTest
+
+
+class FakeReporter:
+ """A fake implementation of a one-file reporter."""
+
+ report_type = "fake report file"
+
+ def __init__(self, output: str = "", error: Optional[Type[Exception]] = None) -> None:
+ self.output = output
+ self.error = error
+ self.morfs: Optional[Iterable[TMorf]] = None
+
+ def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float:
+ """Fake."""
+ self.morfs = morfs
+ outfile.write(self.output)
+ if self.error:
+ raise self.error("You asked for it!")
+ return 17.25
+
+
+class RenderReportTest(CoverageTest):
+ """Tests of render_report."""
+
+ def test_stdout(self) -> None:
+ fake = FakeReporter(output="Hello!\n")
+ msgs: List[str] = []
+ res = render_report("-", fake, [pytest, "coverage"], msgs.append)
+ assert res == 17.25
+ assert fake.morfs == [pytest, "coverage"]
+ assert self.stdout() == "Hello!\n"
+ assert not msgs
+
+ def test_file(self) -> None:
+ fake = FakeReporter(output="Gréètings!\n")
+ msgs: List[str] = []
+ res = render_report("output.txt", fake, [], msgs.append)
+ assert res == 17.25
+ assert self.stdout() == ""
+ with open("output.txt", "rb") as f:
+ assert f.read().rstrip() == b"Gr\xc3\xa9\xc3\xa8tings!"
+ assert msgs == ["Wrote fake report file to output.txt"]
+
+ @pytest.mark.parametrize("error", [CoverageException, ZeroDivisionError])
+ def test_exception(self, error: Type[Exception]) -> None:
+ fake = FakeReporter(error=error)
+ msgs: List[str] = []
+ with pytest.raises(error, match="You asked for it!"):
+ render_report("output.txt", fake, [], msgs.append)
+ assert self.stdout() == ""
+ self.assert_doesnt_exist("output.txt")
+ assert not msgs
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_report.py python-coverage-7.2.7+dfsg1/tests/test_report.py
--- python-coverage-6.5.0+dfsg1/tests/test_report.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_report.py 2023-05-29 19:46:30.000000000 +0000
@@ -1,58 +1,1081 @@
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
-"""Tests for helpers in report.py"""
+"""Test text-based summary reporting for coverage.py"""
+
+from __future__ import annotations
+
+import glob
+import io
+import math
+import os
+import os.path
+import py_compile
+import re
+
+from typing import Tuple
import pytest
-from coverage.exceptions import CoverageException
-from coverage.report import render_report
-from tests.coveragetest import CoverageTest
-
-
-class FakeReporter:
- """A fake implementation of a one-file reporter."""
-
- report_type = "fake report file"
-
- def __init__(self, output="", error=False):
- self.output = output
- self.error = error
- self.morfs = None
-
- def report(self, morfs, outfile):
- """Fake."""
- self.morfs = morfs
- outfile.write(self.output)
- if self.error:
- raise CoverageException("You asked for it!")
-
-
-class RenderReportTest(CoverageTest):
- """Tests of render_report."""
-
- def test_stdout(self):
- fake = FakeReporter(output="Hello!\n")
- msgs = []
- render_report("-", fake, [pytest, "coverage"], msgs.append)
- assert fake.morfs == [pytest, "coverage"]
- assert self.stdout() == "Hello!\n"
- assert not msgs
-
- def test_file(self):
- fake = FakeReporter(output="Gréètings!\n")
- msgs = []
- render_report("output.txt", fake, [], msgs.append)
+import coverage
+from coverage import env
+from coverage.control import Coverage
+from coverage.data import CoverageData
+from coverage.exceptions import ConfigError, NoDataError, NotPython
+from coverage.files import abs_file
+from coverage.report import SummaryReporter
+from coverage.types import TConfigValueIn
+
+from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin
+from tests.helpers import assert_coverage_warnings
+
+
+class SummaryTest(UsingModulesMixin, CoverageTest):
+ """Tests of the text summary reporting for coverage.py."""
+
+ def make_mycode(self) -> None:
+ """Make the mycode.py file when needed."""
+ self.make_file("mycode.py", """\
+ import covmod1
+ import covmodzip1
+ a = 1
+ print('done')
+ """)
+
+ def test_report(self) -> None:
+ self.make_mycode()
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "mycode")
+ assert self.stdout() == 'done\n'
+ report = self.get_report(cov)
+
+ # Name Stmts Miss Cover
+ # ------------------------------------------------------------------
+ # c:/ned/coverage/tests/modules/covmod1.py 2 0 100%
+ # c:/ned/coverage/tests/zipmods.zip/covmodzip1.py 2 0 100%
+ # mycode.py 4 0 100%
+ # ------------------------------------------------------------------
+ # TOTAL 8 0 100%
+
+ assert "/coverage/__init__/" not in report
+ assert "/tests/modules/covmod1.py " in report
+ assert "/tests/zipmods.zip/covmodzip1.py " in report
+ assert "mycode.py " in report
+ assert self.last_line_squeezed(report) == "TOTAL 8 0 100%"
+
+ def test_report_just_one(self) -> None:
+ # Try reporting just one module
+ self.make_mycode()
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "mycode")
+ report = self.get_report(cov, morfs=["mycode.py"])
+
+ # Name Stmts Miss Cover
+ # -------------------------------
+ # mycode.py 4 0 100%
+ # -------------------------------
+ # TOTAL 4 0 100%
+ assert self.line_count(report) == 5
+ assert "/coverage/" not in report
+ assert "/tests/modules/covmod1.py " not in report
+ assert "/tests/zipmods.zip/covmodzip1.py " not in report
+ assert "mycode.py " in report
+ assert self.last_line_squeezed(report) == "TOTAL 4 0 100%"
+
+ def test_report_wildcard(self) -> None:
+ # Try reporting using wildcards to get the modules.
+ self.make_mycode()
+ # Wildcard is handled by shell or cmdline.py, so use real commands
+ self.run_command("coverage run mycode.py")
+ report = self.report_from_command("coverage report my*.py")
+
+ # Name Stmts Miss Cover
+ # -------------------------------
+ # mycode.py 4 0 100%
+ # -------------------------------
+ # TOTAL 4 0 100%
+
+ assert self.line_count(report) == 5
+ assert "/coverage/" not in report
+ assert "/tests/modules/covmod1.py " not in report
+ assert "/tests/zipmods.zip/covmodzip1.py " not in report
+ assert "mycode.py " in report
+ assert self.last_line_squeezed(report) == "TOTAL 4 0 100%"
+
+ def test_report_omitting(self) -> None:
+ # Try reporting while omitting some modules
+ self.make_mycode()
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "mycode")
+ report = self.get_report(cov, omit=[f"{TESTS_DIR}/*", "*/site-packages/*"])
+
+ # Name Stmts Miss Cover
+ # -------------------------------
+ # mycode.py 4 0 100%
+ # -------------------------------
+ # TOTAL 4 0 100%
+
+ assert self.line_count(report) == 5
+ assert "/coverage/" not in report
+ assert "/tests/modules/covmod1.py " not in report
+ assert "/tests/zipmods.zip/covmodzip1.py " not in report
+ assert "mycode.py " in report
+ assert self.last_line_squeezed(report) == "TOTAL 4 0 100%"
+
+ def test_report_including(self) -> None:
+ # Try reporting while including some modules
+ self.make_mycode()
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "mycode")
+ report = self.get_report(cov, include=["mycode*"])
+
+ # Name Stmts Miss Cover
+ # -------------------------------
+ # mycode.py 4 0 100%
+ # -------------------------------
+ # TOTAL 4 0 100%
+
+ assert self.line_count(report) == 5
+ assert "/coverage/" not in report
+ assert "/tests/modules/covmod1.py " not in report
+ assert "/tests/zipmods.zip/covmodzip1.py " not in report
+ assert "mycode.py " in report
+ assert self.last_line_squeezed(report) == "TOTAL 4 0 100%"
+
+ def test_report_include_relative_files_and_path(self) -> None:
+ """
+ Test that when relative_files is True and a relative path to a module
+ is included, coverage is reported for the module.
+
+ Ref: https://github.com/nedbat/coveragepy/issues/1604
+ """
+ self.make_mycode()
+ self.make_file(".coveragerc", """\
+ [run]
+ relative_files = true
+ """)
+ self.make_file("submodule/mycode.py", "import mycode")
+
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "submodule/mycode")
+ report = self.get_report(cov, include="submodule/mycode.py")
+
+ # Name Stmts Miss Cover
+ # ---------------------------------------
+ # submodule/mycode.py 1 0 100%
+ # ---------------------------------------
+ # TOTAL 1 0 100%
+
+ assert "submodule/mycode.py " in report
+ assert self.last_line_squeezed(report) == "TOTAL 1 0 100%"
+
+ def test_report_include_relative_files_and_wildcard_path(self) -> None:
+ self.make_mycode()
+ self.make_file(".coveragerc", """\
+ [run]
+ relative_files = true
+ """)
+ self.make_file("submodule/mycode.py", "import nested.submodule.mycode")
+ self.make_file("nested/submodule/mycode.py", "import mycode")
+
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "submodule/mycode")
+ report = self.get_report(cov, include="*/submodule/mycode.py")
+
+ # Name Stmts Miss Cover
+ # -------------------------------------------------
+ # nested/submodule/mycode.py 1 0 100%
+ # submodule/mycode.py 1 0 100%
+ # -------------------------------------------------
+ # TOTAL 2 0 100%
+
+ reported_files = [line.split()[0] for line in report.splitlines()[2:4]]
+ assert reported_files == [
+ "nested/submodule/mycode.py",
+ "submodule/mycode.py",
+ ]
+
+ def test_omit_files_here(self) -> None:
+ # https://github.com/nedbat/coveragepy/issues/1407
+ self.make_file("foo.py", "")
+ self.make_file("bar/bar.py", "")
+ self.make_file("tests/test_baz.py", """\
+ def test_foo():
+ assert True
+ test_foo()
+ """)
+ self.run_command("coverage run --source=. --omit='./*.py' -m tests.test_baz")
+ report = self.report_from_command("coverage report")
+
+ # Name Stmts Miss Cover
+ # ---------------------------------------
+ # tests/test_baz.py 3 0 100%
+ # ---------------------------------------
+ # TOTAL 3 0 100%
+
+ assert self.line_count(report) == 5
+ assert "foo" not in report
+ assert "bar" not in report
+ assert "tests/test_baz.py" in report
+ assert self.last_line_squeezed(report) == "TOTAL 3 0 100%"
+
+ def test_run_source_vs_report_include(self) -> None:
+ # https://github.com/nedbat/coveragepy/issues/621
+ self.make_file(".coveragerc", """\
+ [run]
+ source = .
+
+ [report]
+ include = mod/*,tests/*
+ """)
+ # It should be OK to use that configuration.
+ cov = coverage.Coverage()
+ with self.assert_warnings(cov, []):
+ cov.start()
+ cov.stop() # pragma: nested
+
+ def test_run_omit_vs_report_omit(self) -> None:
+ # https://github.com/nedbat/coveragepy/issues/622
+ # report:omit shouldn't clobber run:omit.
+ self.make_mycode()
+ self.make_file(".coveragerc", """\
+ [run]
+ omit = */covmodzip1.py
+
+ [report]
+ omit = */covmod1.py
+ """)
+ self.run_command("coverage run mycode.py")
+
+ # Read the data written, to see that the right files have been omitted from running.
+ covdata = CoverageData()
+ covdata.read()
+ files = [os.path.basename(p) for p in covdata.measured_files()]
+ assert "covmod1.py" in files
+ assert "covmodzip1.py" not in files
+
+ def test_report_branches(self) -> None:
+ self.make_file("mybranch.py", """\
+ def branch(x):
+ if x:
+ print("x")
+ return x
+ branch(1)
+ """)
+ cov = coverage.Coverage(source=["."], branch=True)
+ self.start_import_stop(cov, "mybranch")
+ assert self.stdout() == 'x\n'
+ report = self.get_report(cov)
+
+ # Name Stmts Miss Branch BrPart Cover
+ # -----------------------------------------------
+ # mybranch.py 5 0 2 1 86%
+ # -----------------------------------------------
+ # TOTAL 5 0 2 1 86%
+ assert self.line_count(report) == 5
+ assert "mybranch.py " in report
+ assert self.last_line_squeezed(report) == "TOTAL 5 0 2 1 86%"
+
+ def test_report_show_missing(self) -> None:
+ self.make_file("mymissing.py", """\
+ def missing(x, y):
+ if x:
+ print("x")
+ return x
+ if y:
+ print("y")
+ try:
+ print("z")
+ 1/0
+ print("Never!")
+ except ZeroDivisionError:
+ pass
+ return x
+ missing(0, 1)
+ """)
+ cov = coverage.Coverage(source=["."])
+ self.start_import_stop(cov, "mymissing")
+ assert self.stdout() == 'y\nz\n'
+ report = self.get_report(cov, show_missing=True)
+
+ # Name Stmts Miss Cover Missing
+ # --------------------------------------------
+ # mymissing.py 14 3 79% 3-4, 10
+ # --------------------------------------------
+ # TOTAL 14 3 79%
+
+ assert self.line_count(report) == 5
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[2] == "mymissing.py 14 3 79% 3-4, 10"
+ assert squeezed[4] == "TOTAL 14 3 79%"
+
+ def test_report_show_missing_branches(self) -> None:
+ self.make_file("mybranch.py", """\
+ def branch(x, y):
+ if x:
+ print("x")
+ if y:
+ print("y")
+ branch(1, 1)
+ """)
+ cov = coverage.Coverage(branch=True)
+ self.start_import_stop(cov, "mybranch")
+ assert self.stdout() == 'x\ny\n'
+
+ def test_report_show_missing_branches_and_lines(self) -> None:
+ self.make_file("main.py", """\
+ import mybranch
+ """)
+ self.make_file("mybranch.py", """\
+ def branch(x, y, z):
+ if x:
+ print("x")
+ if y:
+ print("y")
+ if z:
+ if x and y:
+ print("z")
+ return x
+ branch(1, 1, 0)
+ """)
+ cov = coverage.Coverage(branch=True)
+ self.start_import_stop(cov, "main")
+ assert self.stdout() == 'x\ny\n'
+
+ def test_report_skip_covered_no_branches(self) -> None:
+ self.make_file("main.py", """
+ import not_covered
+
+ def normal():
+ print("z")
+ normal()
+ """)
+ self.make_file("not_covered.py", """
+ def not_covered():
+ print("n")
+ """)
+ # --fail-under is handled by cmdline.py, use real commands.
+ out = self.run_command("coverage run main.py")
+ assert out == "z\n"
+ report = self.report_from_command("coverage report --skip-covered --fail-under=70")
+
+ # Name Stmts Miss Cover
+ # ------------------------------------
+ # not_covered.py 2 1 50%
+ # ------------------------------------
+ # TOTAL 6 1 83%
+ #
+ # 1 file skipped due to complete coverage.
+
+ assert self.line_count(report) == 7, report
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[2] == "not_covered.py 2 1 50%"
+ assert squeezed[4] == "TOTAL 6 1 83%"
+ assert squeezed[6] == "1 file skipped due to complete coverage."
+ assert self.last_command_status == 0
+
+ def test_report_skip_covered_branches(self) -> None:
+ self.make_file("main.py", """
+ import not_covered, covered
+
+ def normal(z):
+ if z:
+ print("z")
+ normal(True)
+ normal(False)
+ """)
+ self.make_file("not_covered.py", """
+ def not_covered(n):
+ if n:
+ print("n")
+ not_covered(True)
+ """)
+ self.make_file("covered.py", """
+ def foo():
+ pass
+ foo()
+ """)
+ cov = coverage.Coverage(branch=True)
+ self.start_import_stop(cov, "main")
+ assert self.stdout() == "n\nz\n"
+ report = self.get_report(cov, skip_covered=True)
+
+ # Name Stmts Miss Branch BrPart Cover
+ # --------------------------------------------------
+ # not_covered.py 4 0 2 1 83%
+ # --------------------------------------------------
+ # TOTAL 13 0 4 1 94%
+ #
+ # 2 files skipped due to complete coverage.
+
+ assert self.line_count(report) == 7, report
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[2] == "not_covered.py 4 0 2 1 83%"
+ assert squeezed[4] == "TOTAL 13 0 4 1 94%"
+ assert squeezed[6] == "2 files skipped due to complete coverage."
+
+ def test_report_skip_covered_branches_with_totals(self) -> None:
+ self.make_file("main.py", """
+ import not_covered
+ import also_not_run
+
+ def normal(z):
+ if z:
+ print("z")
+ normal(True)
+ normal(False)
+ """)
+ self.make_file("not_covered.py", """
+ def not_covered(n):
+ if n:
+ print("n")
+ not_covered(True)
+ """)
+ self.make_file("also_not_run.py", """
+ def does_not_appear_in_this_film(ni):
+ print("Ni!")
+ """)
+ cov = coverage.Coverage(branch=True)
+ self.start_import_stop(cov, "main")
+ assert self.stdout() == "n\nz\n"
+ report = self.get_report(cov, skip_covered=True)
+
+ # Name Stmts Miss Branch BrPart Cover
+ # --------------------------------------------------
+ # also_not_run.py 2 1 0 0 50%
+ # not_covered.py 4 0 2 1 83%
+ # --------------------------------------------------
+ # TOTAL 13 1 4 1 88%
+ #
+ # 1 file skipped due to complete coverage.
+
+ assert self.line_count(report) == 8, report
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[2] == "also_not_run.py 2 1 0 0 50%"
+ assert squeezed[3] == "not_covered.py 4 0 2 1 83%"
+ assert squeezed[5] == "TOTAL 13 1 4 1 88%"
+ assert squeezed[7] == "1 file skipped due to complete coverage."
+
+ def test_report_skip_covered_all_files_covered(self) -> None:
+ self.make_file("main.py", """
+ def foo():
+ pass
+ foo()
+ """)
+ cov = coverage.Coverage(source=["."], branch=True)
+ self.start_import_stop(cov, "main")
+ assert self.stdout() == ""
+ report = self.get_report(cov, skip_covered=True)
+
+ # Name Stmts Miss Branch BrPart Cover
+ # -----------------------------------------
+ # TOTAL 3 0 0 0 100%
+ #
+ # 1 file skipped due to complete coverage.
+
+ assert self.line_count(report) == 5, report
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[4] == "1 file skipped due to complete coverage."
+
+ report = self.get_report(cov, squeeze=False, skip_covered=True, output_format="markdown")
+
+ # | Name | Stmts | Miss | Branch | BrPart | Cover |
+ # |---------- | -------: | -------: | -------: | -------: | -------: |
+ # | **TOTAL** | **3** | **0** | **0** | **0** | **100%** |
+ #
+ # 1 file skipped due to complete coverage.
+
+ assert self.line_count(report) == 5, report
+ assert report.split("\n")[0] == (
+ '| Name | Stmts | Miss | Branch | BrPart | Cover |'
+ )
+ assert report.split("\n")[1] == (
+ '|---------- | -------: | -------: | -------: | -------: | -------: |'
+ )
+ assert report.split("\n")[2] == (
+ '| **TOTAL** | **3** | **0** | **0** | **0** | **100%** |'
+ )
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[4] == "1 file skipped due to complete coverage."
+
+ total = self.get_report(cov, output_format="total", skip_covered=True)
+ assert total == "100\n"
+
+ def test_report_skip_covered_longfilename(self) -> None:
+ self.make_file("long_______________filename.py", """
+ def foo():
+ pass
+ foo()
+ """)
+ cov = coverage.Coverage(source=["."], branch=True)
+ self.start_import_stop(cov, "long_______________filename")
assert self.stdout() == ""
- with open("output.txt", "rb") as f:
- assert f.read().rstrip() == b"Gr\xc3\xa9\xc3\xa8tings!"
- assert msgs == ["Wrote fake report file to output.txt"]
-
- def test_exception(self):
- fake = FakeReporter(error=True)
- msgs = []
- with pytest.raises(CoverageException, match="You asked for it!"):
- render_report("output.txt", fake, [], msgs.append)
+ report = self.get_report(cov, squeeze=False, skip_covered=True)
+
+ # Name Stmts Miss Branch BrPart Cover
+ # -----------------------------------------
+ # TOTAL 3 0 0 0 100%
+ #
+ # 1 file skipped due to complete coverage.
+
+ assert self.line_count(report) == 5, report
+ lines = self.report_lines(report)
+ assert lines[0] == "Name Stmts Miss Branch BrPart Cover"
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[4] == "1 file skipped due to complete coverage."
+
+ def test_report_skip_covered_no_data(self) -> None:
+ cov = coverage.Coverage()
+ cov.load()
+ with pytest.raises(NoDataError, match="No data to report."):
+ self.get_report(cov, skip_covered=True)
+ self.assert_doesnt_exist(".coverage")
+
+ def test_report_skip_empty(self) -> None:
+ self.make_file("main.py", """
+ import submodule
+
+ def normal():
+ print("z")
+ normal()
+ """)
+ self.make_file("submodule/__init__.py", "")
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "main")
+ assert self.stdout() == "z\n"
+ report = self.get_report(cov, skip_empty=True)
+
+ # Name Stmts Miss Cover
+ # ------------------------------------
+ # main.py 4 0 100%
+ # ------------------------------------
+ # TOTAL 4 0 100%
+ #
+ # 1 empty file skipped.
+
+ assert self.line_count(report) == 7, report
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[2] == "main.py 4 0 100%"
+ assert squeezed[4] == "TOTAL 4 0 100%"
+ assert squeezed[6] == "1 empty file skipped."
+
+ def test_report_skip_empty_no_data(self) -> None:
+ self.make_file("__init__.py", "")
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "__init__")
assert self.stdout() == ""
- self.assert_doesnt_exist("output.txt")
- assert not msgs
+ report = self.get_report(cov, skip_empty=True)
+
+ # Name Stmts Miss Cover
+ # ------------------------------------
+ # TOTAL 0 0 100%
+ #
+ # 1 empty file skipped.
+
+ assert self.line_count(report) == 5, report
+ assert report.split("\n")[2] == "TOTAL 0 0 100%"
+ assert report.split("\n")[4] == "1 empty file skipped."
+
+ def test_report_precision(self) -> None:
+ self.make_file(".coveragerc", """\
+ [report]
+ precision = 3
+ omit = */site-packages/*
+ """)
+ self.make_file("main.py", """
+ import not_covered, covered
+
+ def normal(z):
+ if z:
+ print("z")
+ normal(True)
+ normal(False)
+ """)
+ self.make_file("not_covered.py", """
+ def not_covered(n):
+ if n:
+ print("n")
+ not_covered(True)
+ """)
+ self.make_file("covered.py", """
+ def foo():
+ pass
+ foo()
+ """)
+ cov = coverage.Coverage(branch=True)
+ self.start_import_stop(cov, "main")
+ assert self.stdout() == "n\nz\n"
+ report = self.get_report(cov, squeeze=False)
+
+ # Name Stmts Miss Branch BrPart Cover
+ # ------------------------------------------------------
+ # covered.py 3 0 0 0 100.000%
+ # main.py 6 0 2 0 100.000%
+ # not_covered.py 4 0 2 1 83.333%
+ # ------------------------------------------------------
+ # TOTAL 13 0 4 1 94.118%
+
+ assert self.line_count(report) == 7, report
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[2] == "covered.py 3 0 0 0 100.000%"
+ assert squeezed[4] == "not_covered.py 4 0 2 1 83.333%"
+ assert squeezed[6] == "TOTAL 13 0 4 1 94.118%"
+
+ def test_report_precision_all_zero(self) -> None:
+ self.make_file("not_covered.py", """
+ def not_covered(n):
+ if n:
+ print("n")
+ """)
+ self.make_file("empty.py", "")
+ cov = coverage.Coverage(source=["."])
+ self.start_import_stop(cov, "empty")
+ report = self.get_report(cov, precision=6, squeeze=False)
+
+ # Name Stmts Miss Cover
+ # -----------------------------------------
+ # empty.py 0 0 100.000000%
+ # not_covered.py 3 3 0.000000%
+ # -----------------------------------------
+ # TOTAL 3 3 0.000000%
+
+ assert self.line_count(report) == 6, report
+ assert "empty.py 0 0 100.000000%" in report
+ assert "not_covered.py 3 3 0.000000%" in report
+ assert "TOTAL 3 3 0.000000%" in report
+
+ def test_dotpy_not_python(self) -> None:
+ # We run a .py file, and when reporting, we can't parse it as Python.
+ # We should get an error message in the report.
+
+ self.make_data_file(lines={"mycode.py": [1]})
+ self.make_file("mycode.py", "This isn't python at all!")
+ cov = coverage.Coverage()
+ cov.load()
+ msg = r"Couldn't parse '.*[/\\]mycode.py' as Python source: '.*' at line 1"
+ with pytest.raises(NotPython, match=msg):
+ self.get_report(cov, morfs=["mycode.py"])
+
+ def test_accented_directory(self) -> None:
+ # Make a file with a non-ascii character in the directory name.
+ self.make_file("\xe2/accented.py", "print('accented')")
+ self.make_data_file(lines={abs_file("\xe2/accented.py"): [1]})
+ report_expected = (
+ "Name Stmts Miss Cover\n" +
+ "-----------------------------------\n" +
+ "\xe2/accented.py 1 0 100%\n" +
+ "-----------------------------------\n" +
+ "TOTAL 1 0 100%\n"
+ )
+ cov = coverage.Coverage()
+ cov.load()
+ output = self.get_report(cov, squeeze=False)
+ assert output == report_expected
+
+ def test_accenteddotpy_not_python(self) -> None:
+ # We run a .py file with a non-ascii name, and when reporting, we can't
+ # parse it as Python. We should get an error message in the report.
+
+ self.make_data_file(lines={"accented\xe2.py": [1]})
+ self.make_file("accented\xe2.py", "This isn't python at all!")
+ cov = coverage.Coverage()
+ cov.load()
+ msg = r"Couldn't parse '.*[/\\]accented\xe2.py' as Python source: '.*' at line 1"
+ with pytest.raises(NotPython, match=msg):
+ self.get_report(cov, morfs=["accented\xe2.py"])
+
+ def test_dotpy_not_python_ignored(self) -> None:
+ # We run a .py file, and when reporting, we can't parse it as Python,
+ # but we've said to ignore errors, so there's no error reported,
+ # though we still get a warning.
+ self.make_file("mycode.py", "This isn't python at all!")
+ self.make_data_file(lines={"mycode.py": [1]})
+ cov = coverage.Coverage()
+ cov.load()
+ with pytest.raises(NoDataError, match="No data to report."):
+ with pytest.warns(Warning) as warns:
+ self.get_report(cov, morfs=["mycode.py"], ignore_errors=True)
+ assert_coverage_warnings(
+ warns,
+ re.compile(r"Couldn't parse Python file '.*[/\\]mycode.py' \(couldnt-parse\)"),
+ )
+
+ def test_dothtml_not_python(self) -> None:
+ # We run a .html file, and when reporting, we can't parse it as
+ # Python. Since it wasn't .py, no error is reported.
+
+ # Pretend to run an html file.
+ self.make_file("mycode.html", "This isn't python at all!
")
+ self.make_data_file(lines={"mycode.html": [1]})
+ cov = coverage.Coverage()
+ cov.load()
+ with pytest.raises(NoDataError, match="No data to report."):
+ self.get_report(cov, morfs=["mycode.html"])
+
+ def test_report_no_extension(self) -> None:
+ self.make_file("xxx", """\
+ # This is a python file though it doesn't look like it, like a main script.
+ a = b = c = d = 0
+ a = 3
+ b = 4
+ if not b:
+ c = 6
+ d = 7
+ print(f"xxx: {a} {b} {c} {d}")
+ """)
+ self.make_data_file(lines={abs_file("xxx"): [2, 3, 4, 5, 7, 8]})
+ cov = coverage.Coverage()
+ cov.load()
+ report = self.get_report(cov)
+ assert self.last_line_squeezed(report) == "TOTAL 7 1 86%"
+
+ def test_report_with_chdir(self) -> None:
+ self.make_file("chdir.py", """\
+ import os
+ print("Line One")
+ os.chdir("subdir")
+ print("Line Two")
+ print(open("something").read())
+ """)
+ self.make_file("subdir/something", "hello")
+ out = self.run_command("coverage run --source=. chdir.py")
+ assert out == "Line One\nLine Two\nhello\n"
+ report = self.report_from_command("coverage report")
+ assert self.last_line_squeezed(report) == "TOTAL 5 0 100%"
+ report = self.report_from_command("coverage report --format=markdown")
+ assert self.last_line_squeezed(report) == "| **TOTAL** | **5** | **0** | **100%** |"
+
+ def test_bug_156_file_not_run_should_be_zero(self) -> None:
+ # https://github.com/nedbat/coveragepy/issues/156
+ self.make_file("mybranch.py", """\
+ def branch(x):
+ if x:
+ print("x")
+ return x
+ branch(1)
+ """)
+ self.make_file("main.py", """\
+ print("y")
+ """)
+ cov = coverage.Coverage(branch=True, source=["."])
+ self.start_import_stop(cov, "main")
+ report = self.get_report(cov).splitlines()
+ assert "mybranch.py 5 5 2 0 0%" in report
+
+ def run_TheCode_and_report_it(self) -> str:
+ """A helper for the next few tests."""
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "TheCode")
+ return self.get_report(cov)
+
+ def test_bug_203_mixed_case_listed_twice_with_rc(self) -> None:
+ self.make_file("TheCode.py", "a = 1\n")
+ self.make_file(".coveragerc", "[run]\nsource = .\n")
+
+ report = self.run_TheCode_and_report_it()
+ assert "TheCode" in report
+ assert "thecode" not in report
+
+ def test_bug_203_mixed_case_listed_twice(self) -> None:
+ self.make_file("TheCode.py", "a = 1\n")
+
+ report = self.run_TheCode_and_report_it()
+
+ assert "TheCode" in report
+ assert "thecode" not in report
+
+ @pytest.mark.skipif(not env.WINDOWS, reason=".pyw files are only on Windows.")
+ def test_pyw_files(self) -> None:
+ # https://github.com/nedbat/coveragepy/issues/261
+ self.make_file("start.pyw", """\
+ import mod
+ print("In start.pyw")
+ """)
+ self.make_file("mod.pyw", """\
+ print("In mod.pyw")
+ """)
+ cov = coverage.Coverage()
+ # start_import_stop can't import the .pyw file, so use the long form.
+ cov.start()
+ import start # pragma: nested # pylint: disable=import-error, unused-import
+ cov.stop() # pragma: nested
+
+ report = self.get_report(cov)
+ assert "NoSource" not in report
+ report_lines = report.splitlines()
+ assert "start.pyw 2 0 100%" in report_lines
+ assert "mod.pyw 1 0 100%" in report_lines
+
+ def test_tracing_pyc_file(self) -> None:
+ # Create two Python files.
+ self.make_file("mod.py", "a = 1\n")
+ self.make_file("main.py", "import mod\n")
+
+ # Make one into a .pyc.
+ py_compile.compile("mod.py")
+
+ # Run the program.
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "main")
+
+ report_lines = self.get_report(cov).splitlines()
+ assert "mod.py 1 0 100%" in report_lines
+ report = self.get_report(cov, squeeze=False, output_format="markdown")
+ assert report.split("\n")[3] == "| mod.py | 1 | 0 | 100% |"
+ assert report.split("\n")[4] == "| **TOTAL** | **2** | **0** | **100%** |"
+
+ def test_missing_py_file_during_run(self) -> None:
+ # Create two Python files.
+ self.make_file("mod.py", "a = 1\n")
+ self.make_file("main.py", "import mod\n")
+
+ # Make one into a .pyc, and remove the .py.
+ py_compile.compile("mod.py")
+ os.remove("mod.py")
+
+ # Python 3 puts the .pyc files in a __pycache__ directory, and will
+ # not import from there without source. It will import a .pyc from
+ # the source location though.
+ pycs = glob.glob("__pycache__/mod.*.pyc")
+ assert len(pycs) == 1
+ os.rename(pycs[0], "mod.pyc")
+
+ # Run the program.
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "main")
+
+ # Put back the missing Python file.
+ self.make_file("mod.py", "a = 1\n")
+ report = self.get_report(cov).splitlines()
+ assert "mod.py 1 0 100%" in report
+
+ def test_empty_files(self) -> None:
+ # Shows that empty files like __init__.py are listed as having zero
+ # statements, not one statement.
+ cov = coverage.Coverage(branch=True)
+ cov.start()
+ import usepkgs # pragma: nested # pylint: disable=import-error, unused-import
+ cov.stop() # pragma: nested
+ report = self.get_report(cov)
+ assert "tests/modules/pkg1/__init__.py 1 0 0 0 100%" in report
+ assert "tests/modules/pkg2/__init__.py 0 0 0 0 100%" in report
+ report = self.get_report(cov, squeeze=False, output_format="markdown")
+ # get_report() escapes backslash so we expect forward slash escaped
+ # underscore
+ assert "tests/modules/pkg1//_/_init/_/_.py " in report
+ assert "| 1 | 0 | 0 | 0 | 100% |" in report
+ assert "tests/modules/pkg2//_/_init/_/_.py " in report
+ assert "| 0 | 0 | 0 | 0 | 100% |" in report
+
+ def test_markdown_with_missing(self) -> None:
+ self.make_file("mymissing.py", """\
+ def missing(x, y):
+ if x:
+ print("x")
+ return x
+ if y:
+ print("y")
+ try:
+ print("z")
+ 1/0
+ print("Never!")
+ except ZeroDivisionError:
+ pass
+ return x
+ missing(0, 1)
+ """)
+ cov = coverage.Coverage(source=["."])
+ self.start_import_stop(cov, "mymissing")
+ assert self.stdout() == 'y\nz\n'
+ report = self.get_report(cov, squeeze=False, output_format="markdown", show_missing=True)
+
+ # | Name | Stmts | Miss | Cover | Missing |
+ # |------------- | -------: | -------: | ------: | --------: |
+ # | mymissing.py | 14 | 3 | 79% | 3-4, 10 |
+ # | **TOTAL** | **14** | **3** | **79%** | |
+ assert self.line_count(report) == 4
+ report_lines = report.split("\n")
+ assert report_lines[2] == "| mymissing.py | 14 | 3 | 79% | 3-4, 10 |"
+ assert report_lines[3] == "| **TOTAL** | **14** | **3** | **79%** | |"
+
+ assert self.get_report(cov, output_format="total") == "79\n"
+ assert self.get_report(cov, output_format="total", precision=2) == "78.57\n"
+ assert self.get_report(cov, output_format="total", precision=4) == "78.5714\n"
+
+ def test_bug_1524(self) -> None:
+ self.make_file("bug1524.py", """\
+ class Mine:
+ @property
+ def thing(self) -> int:
+ return 17
+
+ print(Mine().thing)
+ """)
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "bug1524")
+ assert self.stdout() == "17\n"
+ report = self.get_report(cov)
+ report_lines = report.splitlines()
+ assert report_lines[2] == "bug1524.py 5 0 100%"
+
+
+class ReportingReturnValueTest(CoverageTest):
+ """Tests of reporting functions returning values."""
+
+ def run_coverage(self) -> Coverage:
+ """Run coverage on doit.py and return the coverage object."""
+ self.make_file("doit.py", """\
+ a = 1
+ b = 2
+ c = 3
+ d = 4
+ if a > 10:
+ f = 6
+ g = 7
+ """)
+
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "doit")
+ return cov
+
+ def test_report(self) -> None:
+ cov = self.run_coverage()
+ val = cov.report(include="*/doit.py")
+ assert math.isclose(val, 6 / 7 * 100)
+
+ def test_html(self) -> None:
+ cov = self.run_coverage()
+ val = cov.html_report(include="*/doit.py")
+ assert math.isclose(val, 6 / 7 * 100)
+
+ def test_xml(self) -> None:
+ cov = self.run_coverage()
+ val = cov.xml_report(include="*/doit.py")
+ assert math.isclose(val, 6 / 7 * 100)
+
+
+class SummaryReporterConfigurationTest(CoverageTest):
+ """Tests of SummaryReporter."""
+
+ def make_rigged_file(self, filename: str, stmts: int, miss: int) -> None:
+ """Create a file that will have specific results.
+
+ `stmts` and `miss` are ints, the number of statements, and
+ missed statements that should result.
+ """
+ run = stmts - miss - 1
+ dont_run = miss
+ source = ""
+ source += "a = 1\n" * run
+ source += "if a == 99:\n"
+ source += " a = 2\n" * dont_run
+ self.make_file(filename, source)
+
+ def get_summary_text(self, *options: Tuple[str, TConfigValueIn]) -> str:
+ """Get text output from the SummaryReporter.
+
+ The arguments are tuples: (name, value) for Coverage.set_option.
+ """
+ self.make_rigged_file("file1.py", 339, 155)
+ self.make_rigged_file("file2.py", 13, 3)
+ self.make_rigged_file("file10.py", 234, 228)
+ self.make_file("doit.py", "import file1, file2, file10")
+
+ cov = Coverage(source=["."], omit=["doit.py"])
+ self.start_import_stop(cov, "doit")
+ for name, value in options:
+ cov.set_option(name, value)
+ printer = SummaryReporter(cov)
+ destination = io.StringIO()
+ printer.report([], destination)
+ return destination.getvalue()
+
+ def test_test_data(self) -> None:
+ # We use our own test files as test data. Check that our assumptions
+ # about them are still valid. We want the three columns of numbers to
+ # sort in three different orders.
+ report = self.get_summary_text()
+ # Name Stmts Miss Cover
+ # ------------------------------
+ # file1.py 339 155 54%
+ # file2.py 13 3 77%
+ # file10.py 234 228 3%
+ # ------------------------------
+ # TOTAL 586 386 34%
+ lines = report.splitlines()[2:-2]
+ assert len(lines) == 3
+ nums = [list(map(int, l.replace('%', '').split()[1:])) for l in lines]
+ # [
+ # [339, 155, 54],
+ # [ 13, 3, 77],
+ # [234, 228, 3]
+ # ]
+ assert nums[1][0] < nums[2][0] < nums[0][0]
+ assert nums[1][1] < nums[0][1] < nums[2][1]
+ assert nums[2][2] < nums[0][2] < nums[1][2]
+
+ def test_defaults(self) -> None:
+ """Run the report with no configuration options."""
+ report = self.get_summary_text()
+ assert 'Missing' not in report
+ assert 'Branch' not in report
+
+ def test_print_missing(self) -> None:
+ """Run the report printing the missing lines."""
+ report = self.get_summary_text(('report:show_missing', True))
+ assert 'Missing' in report
+ assert 'Branch' not in report
+
+ def assert_ordering(self, text: str, *words: str) -> None:
+ """Assert that the `words` appear in order in `text`."""
+ indexes = list(map(text.find, words))
+ assert -1 not in indexes
+ msg = f"The words {words!r} don't appear in order in {text!r}"
+ assert indexes == sorted(indexes), msg
+
+ def test_default_sort_report(self) -> None:
+ # Sort the text report by the default (Name) column.
+ report = self.get_summary_text()
+ self.assert_ordering(report, "file1.py", "file2.py", "file10.py")
+
+ def test_sort_report_by_name(self) -> None:
+ # Sort the text report explicitly by the Name column.
+ report = self.get_summary_text(('report:sort', 'Name'))
+ self.assert_ordering(report, "file1.py", "file2.py", "file10.py")
+
+ def test_sort_report_by_stmts(self) -> None:
+ # Sort the text report by the Stmts column.
+ report = self.get_summary_text(('report:sort', 'Stmts'))
+ self.assert_ordering(report, "file2.py", "file10.py", "file1.py")
+
+ def test_sort_report_by_missing(self) -> None:
+ # Sort the text report by the Missing column.
+ report = self.get_summary_text(('report:sort', 'Miss'))
+ self.assert_ordering(report, "file2.py", "file1.py", "file10.py")
+
+ def test_sort_report_by_cover(self) -> None:
+ # Sort the text report by the Cover column.
+ report = self.get_summary_text(('report:sort', 'Cover'))
+ self.assert_ordering(report, "file10.py", "file1.py", "file2.py")
+
+ def test_sort_report_by_cover_plus(self) -> None:
+ # Sort the text report by the Cover column, including the explicit + sign.
+ report = self.get_summary_text(('report:sort', '+Cover'))
+ self.assert_ordering(report, "file10.py", "file1.py", "file2.py")
+
+ def test_sort_report_by_cover_reversed(self) -> None:
+ # Sort the text report by the Cover column reversed.
+ report = self.get_summary_text(('report:sort', '-Cover'))
+ self.assert_ordering(report, "file2.py", "file1.py", "file10.py")
+
+ def test_sort_report_by_invalid_option(self) -> None:
+ # Sort the text report by a nonsense column.
+ msg = "Invalid sorting option: 'Xyzzy'"
+ with pytest.raises(ConfigError, match=msg):
+ self.get_summary_text(('report:sort', 'Xyzzy'))
+
+ def test_report_with_invalid_format(self) -> None:
+ # Ask for an invalid format.
+ msg = "Unknown report format choice: 'xyzzy'"
+ with pytest.raises(ConfigError, match=msg):
+ self.get_summary_text(('report:format', 'xyzzy'))
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_results.py python-coverage-7.2.7+dfsg1/tests/test_results.py
--- python-coverage-6.5.0+dfsg1/tests/test_results.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_results.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,12 +3,17 @@
"""Tests for coverage.py's results analysis."""
+from __future__ import annotations
+
import math
+from typing import Dict, Iterable, List, Tuple, cast
+
import pytest
from coverage.exceptions import ConfigError
from coverage.results import format_lines, Numbers, should_fail_under
+from coverage.types import TLineNo
from tests.coveragetest import CoverageTest
@@ -18,14 +23,14 @@
run_in_temp_dir = False
- def test_basic(self):
+ def test_basic(self) -> None:
n1 = Numbers(n_files=1, n_statements=200, n_missing=20)
assert n1.n_statements == 200
assert n1.n_executed == 180
assert n1.n_missing == 20
assert n1.pc_covered == 90
- def test_addition(self):
+ def test_addition(self) -> None:
n1 = Numbers(n_files=1, n_statements=200, n_missing=20)
n2 = Numbers(n_files=1, n_statements=10, n_missing=8)
n3 = n1 + n2
@@ -35,10 +40,10 @@
assert n3.n_missing == 28
assert math.isclose(n3.pc_covered, 86.666666666)
- def test_sum(self):
+ def test_sum(self) -> None:
n1 = Numbers(n_files=1, n_statements=200, n_missing=20)
n2 = Numbers(n_files=1, n_statements=10, n_missing=8)
- n3 = sum([n1, n2])
+ n3 = cast(Numbers, sum([n1, n2]))
assert n3.n_files == 2
assert n3.n_statements == 210
assert n3.n_executed == 182
@@ -55,7 +60,7 @@
(dict(precision=1, n_files=1, n_statements=10000, n_missing=9999), "0.1"),
(dict(precision=1, n_files=1, n_statements=10000, n_missing=10000), "0.0"),
])
- def test_pc_covered_str(self, kwargs, res):
+ def test_pc_covered_str(self, kwargs: Dict[str, int], res: str) -> None:
assert Numbers(**kwargs).pc_covered_str == res
@pytest.mark.parametrize("prec, pc, res", [
@@ -64,7 +69,7 @@
(0, 99.995, "99"),
(2, 99.99995, "99.99"),
])
- def test_display_covered(self, prec, pc, res):
+ def test_display_covered(self, prec: int, pc: float, res: str) -> None:
assert Numbers(precision=prec).display_covered(pc) == res
@pytest.mark.parametrize("prec, width", [
@@ -72,10 +77,10 @@
(1, 5), # 100.0
(4, 8), # 100.0000
])
- def test_pc_str_width(self, prec, width):
+ def test_pc_str_width(self, prec: int, width: int) -> None:
assert Numbers(precision=prec).pc_str_width() == width
- def test_covered_ratio(self):
+ def test_covered_ratio(self) -> None:
n = Numbers(n_files=1, n_statements=200, n_missing=47)
assert n.ratio_covered == (153, 200)
@@ -111,11 +116,11 @@
(99.999, 100, 2, True),
(99.999, 100, 3, True),
])
-def test_should_fail_under(total, fail_under, precision, result):
+def test_should_fail_under(total: float, fail_under: float, precision: int, result: bool) -> None:
assert should_fail_under(float(total), float(fail_under), precision) == result
-def test_should_fail_under_invalid_value():
+def test_should_fail_under_invalid_value() -> None:
with pytest.raises(ConfigError, match=r"fail_under=101"):
should_fail_under(100.0, 101, 0)
@@ -129,7 +134,11 @@
([1, 2, 3, 4, 5], [], ""),
([1, 2, 3, 4, 5], [4], "4"),
])
-def test_format_lines(statements, lines, result):
+def test_format_lines(
+ statements: Iterable[TLineNo],
+ lines: Iterable[TLineNo],
+ result: str,
+) -> None:
assert format_lines(statements, lines) == result
@@ -153,5 +162,10 @@
"1-2, 3->4, 99, 102-104"
),
])
-def test_format_lines_with_arcs(statements, lines, arcs, result):
+def test_format_lines_with_arcs(
+ statements: Iterable[TLineNo],
+ lines: Iterable[TLineNo],
+ arcs: Iterable[Tuple[TLineNo, List[TLineNo]]],
+ result: str,
+) -> None:
assert format_lines(statements, lines, arcs) == result
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_setup.py python-coverage-7.2.7+dfsg1/tests/test_setup.py
--- python-coverage-6.5.0+dfsg1/tests/test_setup.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_setup.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,8 +3,12 @@
"""Tests of miscellaneous stuff."""
+from __future__ import annotations
+
import sys
+from typing import List, cast
+
import coverage
from tests.coveragetest import CoverageTest
@@ -15,12 +19,12 @@
run_in_temp_dir = False
- def setUp(self):
+ def setUp(self) -> None:
super().setUp()
# Force the most restrictive interpretation.
self.set_environ('LC_ALL', 'C')
- def test_metadata(self):
+ def test_metadata(self) -> None:
status, output = self.run_command_status(
"python setup.py --description --version --url --author"
)
@@ -31,19 +35,19 @@
assert "github.com/nedbat/coveragepy" in out[2]
assert "Ned Batchelder" in out[3]
- def test_more_metadata(self):
+ def test_more_metadata(self) -> None:
# Let's be sure we pick up our own setup.py
# CoverageTest restores the original sys.path for us.
sys.path.insert(0, '')
from setup import setup_args
- classifiers = setup_args['classifiers']
+ classifiers = cast(List[str], setup_args['classifiers'])
assert len(classifiers) > 7
assert classifiers[-1].startswith("Development Status ::")
assert "Programming Language :: Python :: %d" % sys.version_info[:1] in classifiers
assert "Programming Language :: Python :: %d.%d" % sys.version_info[:2] in classifiers
- long_description = setup_args['long_description'].splitlines()
+ long_description = cast(str, setup_args['long_description']).splitlines()
assert len(long_description) > 7
assert long_description[0].strip() != ""
assert long_description[-1].strip() != ""
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_summary.py python-coverage-7.2.7+dfsg1/tests/test_summary.py
--- python-coverage-6.5.0+dfsg1/tests/test_summary.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_summary.py 1970-01-01 00:00:00.000000000 +0000
@@ -1,916 +0,0 @@
-# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
-# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
-
-"""Test text-based summary reporting for coverage.py"""
-
-import glob
-import io
-import math
-import os
-import os.path
-import py_compile
-import re
-
-import pytest
-
-import coverage
-from coverage import env
-from coverage.control import Coverage
-from coverage.data import CoverageData
-from coverage.exceptions import ConfigError, NoDataError, NotPython
-from coverage.files import abs_file
-from coverage.summary import SummaryReporter
-
-from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin
-from tests.helpers import assert_coverage_warnings
-
-
-class SummaryTest(UsingModulesMixin, CoverageTest):
- """Tests of the text summary reporting for coverage.py."""
-
- def make_mycode(self):
- """Make the mycode.py file when needed."""
- self.make_file("mycode.py", """\
- import covmod1
- import covmodzip1
- a = 1
- print('done')
- """)
-
- def test_report(self):
- self.make_mycode()
- cov = coverage.Coverage()
- self.start_import_stop(cov, "mycode")
- assert self.stdout() == 'done\n'
- report = self.get_report(cov)
-
- # Name Stmts Miss Cover
- # ------------------------------------------------------------------
- # c:/ned/coverage/tests/modules/covmod1.py 2 0 100%
- # c:/ned/coverage/tests/zipmods.zip/covmodzip1.py 2 0 100%
- # mycode.py 4 0 100%
- # ------------------------------------------------------------------
- # TOTAL 8 0 100%
-
- assert "/coverage/__init__/" not in report
- assert "/tests/modules/covmod1.py " in report
- assert "/tests/zipmods.zip/covmodzip1.py " in report
- assert "mycode.py " in report
- assert self.last_line_squeezed(report) == "TOTAL 8 0 100%"
-
- def test_report_just_one(self):
- # Try reporting just one module
- self.make_mycode()
- cov = coverage.Coverage()
- self.start_import_stop(cov, "mycode")
- report = self.get_report(cov, morfs=["mycode.py"])
-
- # Name Stmts Miss Cover
- # -------------------------------
- # mycode.py 4 0 100%
- # -------------------------------
- # TOTAL 4 0 100%
-
- assert self.line_count(report) == 5
- assert "/coverage/" not in report
- assert "/tests/modules/covmod1.py " not in report
- assert "/tests/zipmods.zip/covmodzip1.py " not in report
- assert "mycode.py " in report
- assert self.last_line_squeezed(report) == "TOTAL 4 0 100%"
-
- def test_report_wildcard(self):
- # Try reporting using wildcards to get the modules.
- self.make_mycode()
- # Wildcard is handled by shell or cmdline.py, so use real commands
- self.run_command("coverage run mycode.py")
- report = self.report_from_command("coverage report my*.py")
-
- # Name Stmts Miss Cover
- # -------------------------------
- # mycode.py 4 0 100%
- # -------------------------------
- # TOTAL 4 0 100%
-
- assert self.line_count(report) == 5
- assert "/coverage/" not in report
- assert "/tests/modules/covmod1.py " not in report
- assert "/tests/zipmods.zip/covmodzip1.py " not in report
- assert "mycode.py " in report
- assert self.last_line_squeezed(report) == "TOTAL 4 0 100%"
-
- def test_report_omitting(self):
- # Try reporting while omitting some modules
- self.make_mycode()
- cov = coverage.Coverage()
- self.start_import_stop(cov, "mycode")
- report = self.get_report(cov, omit=[f"{TESTS_DIR}/*", "*/site-packages/*"])
-
- # Name Stmts Miss Cover
- # -------------------------------
- # mycode.py 4 0 100%
- # -------------------------------
- # TOTAL 4 0 100%
-
- assert self.line_count(report) == 5
- assert "/coverage/" not in report
- assert "/tests/modules/covmod1.py " not in report
- assert "/tests/zipmods.zip/covmodzip1.py " not in report
- assert "mycode.py " in report
- assert self.last_line_squeezed(report) == "TOTAL 4 0 100%"
-
- def test_report_including(self):
- # Try reporting while including some modules
- self.make_mycode()
- cov = coverage.Coverage()
- self.start_import_stop(cov, "mycode")
- report = self.get_report(cov, include=["mycode*"])
-
- # Name Stmts Miss Cover
- # -------------------------------
- # mycode.py 4 0 100%
- # -------------------------------
- # TOTAL 4 0 100%
-
- assert self.line_count(report) == 5
- assert "/coverage/" not in report
- assert "/tests/modules/covmod1.py " not in report
- assert "/tests/zipmods.zip/covmodzip1.py " not in report
- assert "mycode.py " in report
- assert self.last_line_squeezed(report) == "TOTAL 4 0 100%"
-
- def test_run_source_vs_report_include(self):
- # https://github.com/nedbat/coveragepy/issues/621
- self.make_file(".coveragerc", """\
- [run]
- source = .
-
- [report]
- include = mod/*,tests/*
- """)
- # It should be OK to use that configuration.
- cov = coverage.Coverage()
- with self.assert_warnings(cov, []):
- cov.start()
- cov.stop() # pragma: nested
-
- def test_run_omit_vs_report_omit(self):
- # https://github.com/nedbat/coveragepy/issues/622
- # report:omit shouldn't clobber run:omit.
- self.make_mycode()
- self.make_file(".coveragerc", """\
- [run]
- omit = */covmodzip1.py
-
- [report]
- omit = */covmod1.py
- """)
- self.run_command("coverage run mycode.py")
-
- # Read the data written, to see that the right files have been omitted from running.
- covdata = CoverageData()
- covdata.read()
- files = [os.path.basename(p) for p in covdata.measured_files()]
- assert "covmod1.py" in files
- assert "covmodzip1.py" not in files
-
- def test_report_branches(self):
- self.make_file("mybranch.py", """\
- def branch(x):
- if x:
- print("x")
- return x
- branch(1)
- """)
- cov = coverage.Coverage(source=["."], branch=True)
- self.start_import_stop(cov, "mybranch")
- assert self.stdout() == 'x\n'
- report = self.get_report(cov)
-
- # Name Stmts Miss Branch BrPart Cover
- # -----------------------------------------------
- # mybranch.py 5 0 2 1 86%
- # -----------------------------------------------
- # TOTAL 5 0 2 1 86%
-
- assert self.line_count(report) == 5
- assert "mybranch.py " in report
- assert self.last_line_squeezed(report) == "TOTAL 5 0 2 1 86%"
-
- def test_report_show_missing(self):
- self.make_file("mymissing.py", """\
- def missing(x, y):
- if x:
- print("x")
- return x
- if y:
- print("y")
- try:
- print("z")
- 1/0
- print("Never!")
- except ZeroDivisionError:
- pass
- return x
- missing(0, 1)
- """)
- cov = coverage.Coverage(source=["."])
- self.start_import_stop(cov, "mymissing")
- assert self.stdout() == 'y\nz\n'
- report = self.get_report(cov, show_missing=True)
-
- # Name Stmts Miss Cover Missing
- # --------------------------------------------
- # mymissing.py 14 3 79% 3-4, 10
- # --------------------------------------------
- # TOTAL 14 3 79% 3-4, 10
-
- assert self.line_count(report) == 5
- squeezed = self.squeezed_lines(report)
- assert squeezed[2] == "mymissing.py 14 3 79% 3-4, 10"
- assert squeezed[4] == "TOTAL 14 3 79%"
-
- def test_report_show_missing_branches(self):
- self.make_file("mybranch.py", """\
- def branch(x, y):
- if x:
- print("x")
- if y:
- print("y")
- branch(1, 1)
- """)
- cov = coverage.Coverage(branch=True)
- self.start_import_stop(cov, "mybranch")
- assert self.stdout() == 'x\ny\n'
- report = self.get_report(cov, show_missing=True)
-
- # Name Stmts Miss Branch BrPart Cover Missing
- # ----------------------------------------------------------
- # mybranch.py 6 0 4 2 80% 2->4, 4->exit
- # ----------------------------------------------------------
- # TOTAL 6 0 4 2 80%
-
- assert self.line_count(report) == 5
- squeezed = self.squeezed_lines(report)
- assert squeezed[2] == "mybranch.py 6 0 4 2 80% 2->4, 4->exit"
- assert squeezed[4] == "TOTAL 6 0 4 2 80%"
-
- def test_report_show_missing_branches_and_lines(self):
- self.make_file("main.py", """\
- import mybranch
- """)
- self.make_file("mybranch.py", """\
- def branch(x, y, z):
- if x:
- print("x")
- if y:
- print("y")
- if z:
- if x and y:
- print("z")
- return x
- branch(1, 1, 0)
- """)
- cov = coverage.Coverage(branch=True)
- self.start_import_stop(cov, "main")
- assert self.stdout() == 'x\ny\n'
- report_lines = self.get_report(cov, squeeze=False, show_missing=True).splitlines()
-
- expected = [
- 'Name Stmts Miss Branch BrPart Cover Missing',
- '---------------------------------------------------------',
- 'main.py 1 0 0 0 100%',
- 'mybranch.py 10 2 8 3 61% 2->4, 4->6, 7-8',
- '---------------------------------------------------------',
- 'TOTAL 11 2 8 3 63%',
- ]
- assert expected == report_lines
-
- def test_report_skip_covered_no_branches(self):
- self.make_file("main.py", """
- import not_covered
-
- def normal():
- print("z")
- normal()
- """)
- self.make_file("not_covered.py", """
- def not_covered():
- print("n")
- """)
- # --fail-under is handled by cmdline.py, use real commands.
- out = self.run_command("coverage run main.py")
- assert out == "z\n"
- report = self.report_from_command("coverage report --skip-covered --fail-under=70")
-
- # Name Stmts Miss Cover
- # ------------------------------------
- # not_covered.py 2 1 50%
- # ------------------------------------
- # TOTAL 6 1 83%
- #
- # 1 file skipped due to complete coverage.
-
- assert self.line_count(report) == 7, report
- squeezed = self.squeezed_lines(report)
- assert squeezed[2] == "not_covered.py 2 1 50%"
- assert squeezed[4] == "TOTAL 6 1 83%"
- assert squeezed[6] == "1 file skipped due to complete coverage."
- assert self.last_command_status == 0
-
- def test_report_skip_covered_branches(self):
- self.make_file("main.py", """
- import not_covered, covered
-
- def normal(z):
- if z:
- print("z")
- normal(True)
- normal(False)
- """)
- self.make_file("not_covered.py", """
- def not_covered(n):
- if n:
- print("n")
- not_covered(True)
- """)
- self.make_file("covered.py", """
- def foo():
- pass
- foo()
- """)
- cov = coverage.Coverage(branch=True)
- self.start_import_stop(cov, "main")
- assert self.stdout() == "n\nz\n"
- report = self.get_report(cov, skip_covered=True)
-
- # Name Stmts Miss Branch BrPart Cover
- # --------------------------------------------------
- # not_covered.py 4 0 2 1 83%
- # --------------------------------------------------
- # TOTAL 13 0 4 1 94%
- #
- # 2 files skipped due to complete coverage.
-
- assert self.line_count(report) == 7, report
- squeezed = self.squeezed_lines(report)
- assert squeezed[2] == "not_covered.py 4 0 2 1 83%"
- assert squeezed[4] == "TOTAL 13 0 4 1 94%"
- assert squeezed[6] == "2 files skipped due to complete coverage."
-
- def test_report_skip_covered_branches_with_totals(self):
- self.make_file("main.py", """
- import not_covered
- import also_not_run
-
- def normal(z):
- if z:
- print("z")
- normal(True)
- normal(False)
- """)
- self.make_file("not_covered.py", """
- def not_covered(n):
- if n:
- print("n")
- not_covered(True)
- """)
- self.make_file("also_not_run.py", """
- def does_not_appear_in_this_film(ni):
- print("Ni!")
- """)
- cov = coverage.Coverage(branch=True)
- self.start_import_stop(cov, "main")
- assert self.stdout() == "n\nz\n"
- report = self.get_report(cov, skip_covered=True)
-
- # Name Stmts Miss Branch BrPart Cover
- # --------------------------------------------------
- # also_not_run.py 2 1 0 0 50%
- # not_covered.py 4 0 2 1 83%
- # --------------------------------------------------
- # TOTAL 13 1 4 1 88%
- #
- # 1 file skipped due to complete coverage.
-
- assert self.line_count(report) == 8, report
- squeezed = self.squeezed_lines(report)
- assert squeezed[2] == "also_not_run.py 2 1 0 0 50%"
- assert squeezed[3] == "not_covered.py 4 0 2 1 83%"
- assert squeezed[5] == "TOTAL 13 1 4 1 88%"
- assert squeezed[7] == "1 file skipped due to complete coverage."
-
- def test_report_skip_covered_all_files_covered(self):
- self.make_file("main.py", """
- def foo():
- pass
- foo()
- """)
- cov = coverage.Coverage(source=["."], branch=True)
- self.start_import_stop(cov, "main")
- assert self.stdout() == ""
- report = self.get_report(cov, skip_covered=True)
-
- # Name Stmts Miss Branch BrPart Cover
- # -------------------------------------------
- # -----------------------------------------
- # TOTAL 3 0 0 0 100%
- #
- # 1 file skipped due to complete coverage.
-
- assert self.line_count(report) == 6, report
- squeezed = self.squeezed_lines(report)
- assert squeezed[5] == "1 file skipped due to complete coverage."
-
- def test_report_skip_covered_longfilename(self):
- self.make_file("long_______________filename.py", """
- def foo():
- pass
- foo()
- """)
- cov = coverage.Coverage(source=["."], branch=True)
- self.start_import_stop(cov, "long_______________filename")
- assert self.stdout() == ""
- report = self.get_report(cov, squeeze=False, skip_covered=True)
-
- # Name Stmts Miss Branch BrPart Cover
- # -----------------------------------------
- # -----------------------------------------
- # TOTAL 3 0 0 0 100%
- #
- # 1 file skipped due to complete coverage.
-
- assert self.line_count(report) == 6, report
- lines = self.report_lines(report)
- assert lines[0] == "Name Stmts Miss Branch BrPart Cover"
- squeezed = self.squeezed_lines(report)
- assert squeezed[5] == "1 file skipped due to complete coverage."
-
- def test_report_skip_covered_no_data(self):
- cov = coverage.Coverage()
- cov.load()
- with pytest.raises(NoDataError, match="No data to report."):
- self.get_report(cov, skip_covered=True)
- self.assert_doesnt_exist(".coverage")
-
- def test_report_skip_empty(self):
- self.make_file("main.py", """
- import submodule
-
- def normal():
- print("z")
- normal()
- """)
- self.make_file("submodule/__init__.py", "")
- cov = coverage.Coverage()
- self.start_import_stop(cov, "main")
- assert self.stdout() == "z\n"
- report = self.get_report(cov, skip_empty=True)
-
- # Name Stmts Miss Cover
- # ------------------------------------
- # main.py 4 0 100%
- # ------------------------------------
- # TOTAL 4 0 100%
- #
- # 1 empty file skipped.
-
- assert self.line_count(report) == 7, report
- squeezed = self.squeezed_lines(report)
- assert squeezed[2] == "main.py 4 0 100%"
- assert squeezed[4] == "TOTAL 4 0 100%"
- assert squeezed[6] == "1 empty file skipped."
-
- def test_report_skip_empty_no_data(self):
- self.make_file("__init__.py", "")
- cov = coverage.Coverage()
- self.start_import_stop(cov, "__init__")
- assert self.stdout() == ""
- report = self.get_report(cov, skip_empty=True)
-
- # Name Stmts Miss Cover
- # ------------------------------------
- #
- # 1 empty file skipped.
-
- assert self.line_count(report) == 6, report
- squeezed = self.squeezed_lines(report)
- assert squeezed[3] == "TOTAL 0 0 100%"
- assert squeezed[5] == "1 empty file skipped."
-
- def test_report_precision(self):
- self.make_file(".coveragerc", """\
- [report]
- precision = 3
- omit = */site-packages/*
- """)
- self.make_file("main.py", """
- import not_covered, covered
-
- def normal(z):
- if z:
- print("z")
- normal(True)
- normal(False)
- """)
- self.make_file("not_covered.py", """
- def not_covered(n):
- if n:
- print("n")
- not_covered(True)
- """)
- self.make_file("covered.py", """
- def foo():
- pass
- foo()
- """)
- cov = coverage.Coverage(branch=True)
- self.start_import_stop(cov, "main")
- assert self.stdout() == "n\nz\n"
- report = self.get_report(cov)
-
- # Name Stmts Miss Branch BrPart Cover
- # ------------------------------------------------------
- # covered.py 3 0 0 0 100.000%
- # main.py 6 0 2 0 100.000%
- # not_covered.py 4 0 2 1 83.333%
- # ------------------------------------------------------
- # TOTAL 13 0 4 1 94.118%
-
- assert self.line_count(report) == 7, report
- squeezed = self.squeezed_lines(report)
- assert squeezed[2] == "covered.py 3 0 0 0 100.000%"
- assert squeezed[4] == "not_covered.py 4 0 2 1 83.333%"
- assert squeezed[6] == "TOTAL 13 0 4 1 94.118%"
-
- def test_dotpy_not_python(self):
- # We run a .py file, and when reporting, we can't parse it as Python.
- # We should get an error message in the report.
-
- self.make_data_file(lines={"mycode.py": [1]})
- self.make_file("mycode.py", "This isn't python at all!")
- cov = coverage.Coverage()
- cov.load()
- msg = r"Couldn't parse '.*[/\\]mycode.py' as Python source: '.*' at line 1"
- with pytest.raises(NotPython, match=msg):
- self.get_report(cov, morfs=["mycode.py"])
-
- def test_accented_directory(self):
- # Make a file with a non-ascii character in the directory name.
- self.make_file("\xe2/accented.py", "print('accented')")
- self.make_data_file(lines={abs_file("\xe2/accented.py"): [1]})
- report_expected = (
- "Name Stmts Miss Cover\n" +
- "-----------------------------------\n" +
- "\xe2/accented.py 1 0 100%\n" +
- "-----------------------------------\n" +
- "TOTAL 1 0 100%\n"
- )
-
- cov = coverage.Coverage()
- cov.load()
- output = self.get_report(cov, squeeze=False)
- assert output == report_expected
-
- @pytest.mark.skipif(env.JYTHON, reason="Jython doesn't like accented file names")
- def test_accenteddotpy_not_python(self):
- # We run a .py file with a non-ascii name, and when reporting, we can't
- # parse it as Python. We should get an error message in the report.
-
- self.make_data_file(lines={"accented\xe2.py": [1]})
- self.make_file("accented\xe2.py", "This isn't python at all!")
- cov = coverage.Coverage()
- cov.load()
- msg = r"Couldn't parse '.*[/\\]accented\xe2.py' as Python source: '.*' at line 1"
- with pytest.raises(NotPython, match=msg):
- self.get_report(cov, morfs=["accented\xe2.py"])
-
- def test_dotpy_not_python_ignored(self):
- # We run a .py file, and when reporting, we can't parse it as Python,
- # but we've said to ignore errors, so there's no error reported,
- # though we still get a warning.
- self.make_file("mycode.py", "This isn't python at all!")
- self.make_data_file(lines={"mycode.py": [1]})
- cov = coverage.Coverage()
- cov.load()
- with pytest.raises(NoDataError, match="No data to report."):
- with pytest.warns(Warning) as warns:
- self.get_report(cov, morfs=["mycode.py"], ignore_errors=True)
- assert_coverage_warnings(
- warns,
- re.compile(r"Couldn't parse Python file '.*[/\\]mycode.py' \(couldnt-parse\)"),
- )
-
- def test_dothtml_not_python(self):
- # We run a .html file, and when reporting, we can't parse it as
- # Python. Since it wasn't .py, no error is reported.
-
- # Pretend to run an html file.
- self.make_file("mycode.html", "This isn't python at all!
")
- self.make_data_file(lines={"mycode.html": [1]})
- cov = coverage.Coverage()
- cov.load()
- with pytest.raises(NoDataError, match="No data to report."):
- self.get_report(cov, morfs=["mycode.html"])
-
- def test_report_no_extension(self):
- self.make_file("xxx", """\
- # This is a python file though it doesn't look like it, like a main script.
- a = b = c = d = 0
- a = 3
- b = 4
- if not b:
- c = 6
- d = 7
- print(f"xxx: {a} {b} {c} {d}")
- """)
- self.make_data_file(lines={abs_file("xxx"): [2, 3, 4, 5, 7, 8]})
- cov = coverage.Coverage()
- cov.load()
- report = self.get_report(cov)
- assert self.last_line_squeezed(report) == "TOTAL 7 1 86%"
-
- def test_report_with_chdir(self):
- self.make_file("chdir.py", """\
- import os
- print("Line One")
- os.chdir("subdir")
- print("Line Two")
- print(open("something").read())
- """)
- self.make_file("subdir/something", "hello")
- out = self.run_command("coverage run --source=. chdir.py")
- assert out == "Line One\nLine Two\nhello\n"
- report = self.report_from_command("coverage report")
- assert self.last_line_squeezed(report) == "TOTAL 5 0 100%"
-
- def test_bug_156_file_not_run_should_be_zero(self):
- # https://github.com/nedbat/coveragepy/issues/156
- self.make_file("mybranch.py", """\
- def branch(x):
- if x:
- print("x")
- return x
- branch(1)
- """)
- self.make_file("main.py", """\
- print("y")
- """)
- cov = coverage.Coverage(branch=True, source=["."])
- self.start_import_stop(cov, "main")
- report = self.get_report(cov).splitlines()
- assert "mybranch.py 5 5 2 0 0%" in report
-
- def run_TheCode_and_report_it(self):
- """A helper for the next few tests."""
- cov = coverage.Coverage()
- self.start_import_stop(cov, "TheCode")
- return self.get_report(cov)
-
- def test_bug_203_mixed_case_listed_twice_with_rc(self):
- self.make_file("TheCode.py", "a = 1\n")
- self.make_file(".coveragerc", "[run]\nsource = .\n")
-
- report = self.run_TheCode_and_report_it()
-
- assert "TheCode" in report
- assert "thecode" not in report
-
- def test_bug_203_mixed_case_listed_twice(self):
- self.make_file("TheCode.py", "a = 1\n")
-
- report = self.run_TheCode_and_report_it()
-
- assert "TheCode" in report
- assert "thecode" not in report
-
- @pytest.mark.skipif(not env.WINDOWS, reason=".pyw files are only on Windows.")
- def test_pyw_files(self):
- # https://github.com/nedbat/coveragepy/issues/261
- self.make_file("start.pyw", """\
- import mod
- print("In start.pyw")
- """)
- self.make_file("mod.pyw", """\
- print("In mod.pyw")
- """)
- cov = coverage.Coverage()
- # start_import_stop can't import the .pyw file, so use the long form.
- cov.start()
- import start # pragma: nested # pylint: disable=import-error, unused-import
- cov.stop() # pragma: nested
-
- report = self.get_report(cov)
- assert "NoSource" not in report
- report = report.splitlines()
- assert "start.pyw 2 0 100%" in report
- assert "mod.pyw 1 0 100%" in report
-
- def test_tracing_pyc_file(self):
- # Create two Python files.
- self.make_file("mod.py", "a = 1\n")
- self.make_file("main.py", "import mod\n")
-
- # Make one into a .pyc.
- py_compile.compile("mod.py")
-
- # Run the program.
- cov = coverage.Coverage()
- self.start_import_stop(cov, "main")
-
- report = self.get_report(cov).splitlines()
- assert "mod.py 1 0 100%" in report
-
- def test_missing_py_file_during_run(self):
- # Create two Python files.
- self.make_file("mod.py", "a = 1\n")
- self.make_file("main.py", "import mod\n")
-
- # Make one into a .pyc, and remove the .py.
- py_compile.compile("mod.py")
- os.remove("mod.py")
-
- # Python 3 puts the .pyc files in a __pycache__ directory, and will
- # not import from there without source. It will import a .pyc from
- # the source location though.
- if not env.JYTHON:
- pycs = glob.glob("__pycache__/mod.*.pyc")
- assert len(pycs) == 1
- os.rename(pycs[0], "mod.pyc")
-
- # Run the program.
- cov = coverage.Coverage()
- self.start_import_stop(cov, "main")
-
- # Put back the missing Python file.
- self.make_file("mod.py", "a = 1\n")
- report = self.get_report(cov).splitlines()
- assert "mod.py 1 0 100%" in report
-
- def test_empty_files(self):
- # Shows that empty files like __init__.py are listed as having zero
- # statements, not one statement.
- cov = coverage.Coverage(branch=True)
- cov.start()
- import usepkgs # pragma: nested # pylint: disable=import-error, unused-import
- cov.stop() # pragma: nested
- report = self.get_report(cov)
- assert "tests/modules/pkg1/__init__.py 1 0 0 0 100%" in report
- assert "tests/modules/pkg2/__init__.py 0 0 0 0 100%" in report
-
-
-class ReportingReturnValueTest(CoverageTest):
- """Tests of reporting functions returning values."""
-
- def run_coverage(self):
- """Run coverage on doit.py and return the coverage object."""
- self.make_file("doit.py", """\
- a = 1
- b = 2
- c = 3
- d = 4
- if a > 10:
- f = 6
- g = 7
- """)
-
- cov = coverage.Coverage()
- self.start_import_stop(cov, "doit")
- return cov
-
- def test_report(self):
- cov = self.run_coverage()
- val = cov.report(include="*/doit.py")
- assert math.isclose(val, 6 / 7 * 100)
-
- def test_html(self):
- cov = self.run_coverage()
- val = cov.html_report(include="*/doit.py")
- assert math.isclose(val, 6 / 7 * 100)
-
- def test_xml(self):
- cov = self.run_coverage()
- val = cov.xml_report(include="*/doit.py")
- assert math.isclose(val, 6 / 7 * 100)
-
-
-class SummaryReporterConfigurationTest(CoverageTest):
- """Tests of SummaryReporter."""
-
- def make_rigged_file(self, filename, stmts, miss):
- """Create a file that will have specific results.
-
- `stmts` and `miss` are ints, the number of statements, and
- missed statements that should result.
- """
- run = stmts - miss - 1
- dont_run = miss
- source = ""
- source += "a = 1\n" * run
- source += "if a == 99:\n"
- source += " a = 2\n" * dont_run
- self.make_file(filename, source)
-
- def get_summary_text(self, *options):
- """Get text output from the SummaryReporter.
-
- The arguments are tuples: (name, value) for Coverage.set_option.
- """
- self.make_rigged_file("file1.py", 339, 155)
- self.make_rigged_file("file2.py", 13, 3)
- self.make_rigged_file("file10.py", 234, 228)
- self.make_file("doit.py", "import file1, file2, file10")
-
- cov = Coverage(source=["."], omit=["doit.py"])
- self.start_import_stop(cov, "doit")
- for name, value in options:
- cov.set_option(name, value)
- printer = SummaryReporter(cov)
- destination = io.StringIO()
- printer.report([], destination)
- return destination.getvalue()
-
- def test_test_data(self):
- # We use our own test files as test data. Check that our assumptions
- # about them are still valid. We want the three columns of numbers to
- # sort in three different orders.
- report = self.get_summary_text()
- print(report)
- # Name Stmts Miss Cover
- # ------------------------------
- # file1.py 339 155 54%
- # file2.py 13 3 77%
- # file10.py 234 228 3%
- # ------------------------------
- # TOTAL 586 386 34%
-
- lines = report.splitlines()[2:-2]
- assert len(lines) == 3
- nums = [list(map(int, l.replace('%', '').split()[1:])) for l in lines]
- # [
- # [339, 155, 54],
- # [ 13, 3, 77],
- # [234, 228, 3]
- # ]
- assert nums[1][0] < nums[2][0] < nums[0][0]
- assert nums[1][1] < nums[0][1] < nums[2][1]
- assert nums[2][2] < nums[0][2] < nums[1][2]
-
- def test_defaults(self):
- """Run the report with no configuration options."""
- report = self.get_summary_text()
- assert 'Missing' not in report
- assert 'Branch' not in report
-
- def test_print_missing(self):
- """Run the report printing the missing lines."""
- report = self.get_summary_text(('report:show_missing', True))
- assert 'Missing' in report
- assert 'Branch' not in report
-
- def assert_ordering(self, text, *words):
- """Assert that the `words` appear in order in `text`."""
- indexes = list(map(text.find, words))
- assert -1 not in indexes
- msg = f"The words {words!r} don't appear in order in {text!r}"
- assert indexes == sorted(indexes), msg
-
- def test_default_sort_report(self):
- # Sort the text report by the default (Name) column.
- report = self.get_summary_text()
- self.assert_ordering(report, "file1.py", "file2.py", "file10.py")
-
- def test_sort_report_by_name(self):
- # Sort the text report explicitly by the Name column.
- report = self.get_summary_text(('report:sort', 'Name'))
- self.assert_ordering(report, "file1.py", "file2.py", "file10.py")
-
- def test_sort_report_by_stmts(self):
- # Sort the text report by the Stmts column.
- report = self.get_summary_text(('report:sort', 'Stmts'))
- self.assert_ordering(report, "file2.py", "file10.py", "file1.py")
-
- def test_sort_report_by_missing(self):
- # Sort the text report by the Missing column.
- report = self.get_summary_text(('report:sort', 'Miss'))
- self.assert_ordering(report, "file2.py", "file1.py", "file10.py")
-
- def test_sort_report_by_cover(self):
- # Sort the text report by the Cover column.
- report = self.get_summary_text(('report:sort', 'Cover'))
- self.assert_ordering(report, "file10.py", "file1.py", "file2.py")
-
- def test_sort_report_by_cover_plus(self):
- # Sort the text report by the Cover column, including the explicit + sign.
- report = self.get_summary_text(('report:sort', '+Cover'))
- self.assert_ordering(report, "file10.py", "file1.py", "file2.py")
-
- def test_sort_report_by_cover_reversed(self):
- # Sort the text report by the Cover column reversed.
- report = self.get_summary_text(('report:sort', '-Cover'))
- self.assert_ordering(report, "file2.py", "file1.py", "file10.py")
-
- def test_sort_report_by_invalid_option(self):
- # Sort the text report by a nonsense column.
- msg = "Invalid sorting option: 'Xyzzy'"
- with pytest.raises(ConfigError, match=msg):
- self.get_summary_text(('report:sort', 'Xyzzy'))
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_templite.py python-coverage-7.2.7+dfsg1/tests/test_templite.py
--- python-coverage-6.5.0+dfsg1/tests/test_templite.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_templite.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,8 +3,13 @@
"""Tests for coverage.templite."""
+from __future__ import annotations
+
import re
+from types import SimpleNamespace
+from typing import Any, ContextManager, Dict, List, Optional
+
import pytest
from coverage.templite import Templite, TempliteSyntaxError, TempliteValueError
@@ -13,23 +18,18 @@
# pylint: disable=possibly-unused-variable
-class AnyOldObject:
- """Simple testing object.
-
- Use keyword arguments in the constructor to set attributes on the object.
-
- """
- def __init__(self, **attrs):
- for n, v in attrs.items():
- setattr(self, n, v)
-
class TempliteTest(CoverageTest):
"""Tests for Templite."""
run_in_temp_dir = False
- def try_render(self, text, ctx=None, result=None):
+ def try_render(
+ self,
+ text: str,
+ ctx: Optional[Dict[str, Any]] = None,
+ result: Optional[str] = None,
+ ) -> None:
"""Render `text` through `ctx`, and it had better be `result`.
Result defaults to None so we can shorten the calls where we expect
@@ -42,30 +42,30 @@
assert result is not None
assert actual == result
- def assertSynErr(self, msg):
+ def assertSynErr(self, msg: str) -> ContextManager[None]:
"""Assert that a `TempliteSyntaxError` will happen.
A context manager, and the message should be `msg`.
"""
pat = "^" + re.escape(msg) + "$"
- return pytest.raises(TempliteSyntaxError, match=pat)
+ return pytest.raises(TempliteSyntaxError, match=pat) # type: ignore
- def test_passthrough(self):
+ def test_passthrough(self) -> None:
# Strings without variables are passed through unchanged.
assert Templite("Hello").render() == "Hello"
assert Templite("Hello, 20% fun time!").render() == "Hello, 20% fun time!"
- def test_variables(self):
+ def test_variables(self) -> None:
# Variables use {{var}} syntax.
self.try_render("Hello, {{name}}!", {'name':'Ned'}, "Hello, Ned!")
- def test_undefined_variables(self):
+ def test_undefined_variables(self) -> None:
# Using undefined names is an error.
with pytest.raises(Exception, match="'name'"):
self.try_render("Hi, {{name}}!")
- def test_pipes(self):
+ def test_pipes(self) -> None:
# Variables can be filtered with pipes.
data = {
'name': 'Ned',
@@ -77,7 +77,7 @@
# Pipes can be concatenated.
self.try_render("Hello, {{name|upper|second}}!", data, "Hello, E!")
- def test_reusability(self):
+ def test_reusability(self) -> None:
# A single Templite can be used more than once with different data.
globs = {
'upper': lambda x: x.upper(),
@@ -88,30 +88,30 @@
assert template.render({'name':'Ned'}) == "This is NED!"
assert template.render({'name':'Ben'}) == "This is BEN!"
- def test_attribute(self):
+ def test_attribute(self) -> None:
# Variables' attributes can be accessed with dots.
- obj = AnyOldObject(a="Ay")
+ obj = SimpleNamespace(a="Ay")
self.try_render("{{obj.a}}", locals(), "Ay")
- obj2 = AnyOldObject(obj=obj, b="Bee")
+ obj2 = SimpleNamespace(obj=obj, b="Bee")
self.try_render("{{obj2.obj.a}} {{obj2.b}}", locals(), "Ay Bee")
- def test_member_function(self):
+ def test_member_function(self) -> None:
# Variables' member functions can be used, as long as they are nullary.
- class WithMemberFns(AnyOldObject):
+ class WithMemberFns(SimpleNamespace):
"""A class to try out member function access."""
- def ditto(self):
+ def ditto(self) -> str:
"""Return twice the .txt attribute."""
- return self.txt + self.txt
+ return self.txt + self.txt # type: ignore
obj = WithMemberFns(txt="Once")
self.try_render("{{obj.ditto}}", locals(), "OnceOnce")
- def test_item_access(self):
+ def test_item_access(self) -> None:
# Variables' items can be used.
d = {'a':17, 'b':23}
self.try_render("{{d.a}} < {{d.b}}", locals(), "17 < 23")
- def test_loops(self):
+ def test_loops(self) -> None:
# Loops work like in Django.
nums = [1,2,3,4]
self.try_render(
@@ -120,7 +120,7 @@
"Look: 1, 2, 3, 4, done."
)
# Loop iterables can be filtered.
- def rev(l):
+ def rev(l: List[int]) -> List[int]:
"""Return the reverse of `l`."""
l = l[:]
l.reverse()
@@ -132,21 +132,21 @@
"Look: 4, 3, 2, 1, done."
)
- def test_empty_loops(self):
+ def test_empty_loops(self) -> None:
self.try_render(
"Empty: {% for n in nums %}{{n}}, {% endfor %}done.",
{'nums':[]},
"Empty: done."
)
- def test_multiline_loops(self):
+ def test_multiline_loops(self) -> None:
self.try_render(
"Look: \n{% for n in nums %}\n{{n}}, \n{% endfor %}done.",
{'nums':[1,2,3]},
"Look: \n\n1, \n\n2, \n\n3, \ndone."
)
- def test_multiple_loops(self):
+ def test_multiple_loops(self) -> None:
self.try_render(
"{% for n in nums %}{{n}}{% endfor %} and " +
"{% for n in nums %}{{n}}{% endfor %}",
@@ -154,7 +154,7 @@
"123 and 123"
)
- def test_comments(self):
+ def test_comments(self) -> None:
# Single-line comments work:
self.try_render(
"Hello, {# Name goes here: #}{{name}}!",
@@ -166,7 +166,7 @@
{'name':'Ned'}, "Hello, Ned!"
)
- def test_if(self):
+ def test_if(self) -> None:
self.try_render(
"Hi, {% if ned %}NED{% endif %}{% if ben %}BEN{% endif %}!",
{'ned': 1, 'ben': 0},
@@ -193,10 +193,10 @@
"Hi, NEDBEN!"
)
- def test_complex_if(self):
- class Complex(AnyOldObject):
+ def test_complex_if(self) -> None:
+ class Complex(SimpleNamespace):
"""A class to try out complex data access."""
- def getit(self):
+ def getit(self): # type: ignore
"""Return it."""
return self.it
obj = Complex(it={'x':"Hello", 'y': 0})
@@ -210,7 +210,7 @@
"@XS!"
)
- def test_loop_if(self):
+ def test_loop_if(self) -> None:
self.try_render(
"@{% for n in nums %}{% if n %}Z{% endif %}{{n}}{% endfor %}!",
{'nums': [0,1,2]},
@@ -227,7 +227,7 @@
"X!"
)
- def test_nested_loops(self):
+ def test_nested_loops(self) -> None:
self.try_render(
"@" +
"{% for n in nums %}" +
@@ -238,7 +238,7 @@
"@a0b0c0a1b1c1a2b2c2!"
)
- def test_whitespace_handling(self):
+ def test_whitespace_handling(self) -> None:
self.try_render(
"@{% for n in nums %}\n" +
" {% for a in abc %}{{a}}{{n}}{% endfor %}\n" +
@@ -268,7 +268,7 @@
)
self.try_render(" hello ", {}, " hello ")
- def test_eat_whitespace(self):
+ def test_eat_whitespace(self) -> None:
self.try_render(
"Hey!\n" +
"{% joined %}\n" +
@@ -286,14 +286,14 @@
"Hey!\n@XYa0XYb0XYc0XYa1XYb1XYc1XYa2XYb2XYc2!\n"
)
- def test_non_ascii(self):
+ def test_non_ascii(self) -> None:
self.try_render(
"{{where}} ollǝɥ",
{ 'where': 'ǝɹǝɥʇ' },
"ǝɹǝɥʇ ollǝɥ"
)
- def test_exception_during_evaluation(self):
+ def test_exception_during_evaluation(self) -> None:
# TypeError: Couldn't evaluate {{ foo.bar.baz }}:
regex = "^Couldn't evaluate None.bar$"
with pytest.raises(TempliteValueError, match=regex):
@@ -301,7 +301,7 @@
"Hey {{foo.bar.baz}} there", {'foo': None}, "Hey ??? there"
)
- def test_bad_names(self):
+ def test_bad_names(self) -> None:
with self.assertSynErr("Not a valid name: 'var%&!@'"):
self.try_render("Wat: {{ var%&!@ }}")
with self.assertSynErr("Not a valid name: 'filter%&!@'"):
@@ -309,17 +309,17 @@
with self.assertSynErr("Not a valid name: '@'"):
self.try_render("Wat: {% for @ in x %}{% endfor %}")
- def test_bogus_tag_syntax(self):
+ def test_bogus_tag_syntax(self) -> None:
with self.assertSynErr("Don't understand tag: 'bogus'"):
self.try_render("Huh: {% bogus %}!!{% endbogus %}??")
- def test_malformed_if(self):
+ def test_malformed_if(self) -> None:
with self.assertSynErr("Don't understand if: '{% if %}'"):
self.try_render("Buh? {% if %}hi!{% endif %}")
with self.assertSynErr("Don't understand if: '{% if this or that %}'"):
self.try_render("Buh? {% if this or that %}hi!{% endif %}")
- def test_malformed_for(self):
+ def test_malformed_for(self) -> None:
with self.assertSynErr("Don't understand for: '{% for %}'"):
self.try_render("Weird: {% for %}loop{% endfor %}")
with self.assertSynErr("Don't understand for: '{% for x from y %}'"):
@@ -327,7 +327,7 @@
with self.assertSynErr("Don't understand for: '{% for x, y in z %}'"):
self.try_render("Weird: {% for x, y in z %}loop{% endfor %}")
- def test_bad_nesting(self):
+ def test_bad_nesting(self) -> None:
with self.assertSynErr("Unmatched action tag: 'if'"):
self.try_render("{% if x %}X")
with self.assertSynErr("Mismatched end tag: 'for'"):
@@ -335,7 +335,7 @@
with self.assertSynErr("Too many ends: '{% endif %}'"):
self.try_render("{% if x %}{% endif %}{% endif %}")
- def test_malformed_end(self):
+ def test_malformed_end(self) -> None:
with self.assertSynErr("Don't understand end: '{% end if %}'"):
self.try_render("{% if x %}X{% end if %}")
with self.assertSynErr("Don't understand end: '{% endif now %}'"):
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_testing.py python-coverage-7.2.7+dfsg1/tests/test_testing.py
--- python-coverage-6.5.0+dfsg1/tests/test_testing.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_testing.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,33 +3,37 @@
"""Tests that our test infrastructure is really working!"""
+from __future__ import annotations
+
import datetime
import os
import re
import sys
import warnings
+from typing import List, Tuple
+
import pytest
import coverage
-from coverage import tomlconfig
from coverage.exceptions import CoverageWarning
from coverage.files import actual_path
+from coverage.types import TArc
from tests.coveragetest import CoverageTest
from tests.helpers import (
arcs_to_arcz_repr, arcz_to_arcs, assert_count_equal, assert_coverage_warnings,
- CheckUniqueFilenames, re_lines, re_lines_text, re_line, without_module,
+ CheckUniqueFilenames, re_lines, re_lines_text, re_line,
)
-def test_xdist_sys_path_nuttiness_is_fixed():
+def test_xdist_sys_path_nuttiness_is_fixed() -> None:
# See conftest.py:fix_xdist_sys_path
assert sys.path[1] != ''
assert os.environ.get('PYTHONPATH') is None
-def test_assert_count_equal():
+def test_assert_count_equal() -> None:
assert_count_equal(set(), set())
assert_count_equal({"a": 1, "b": 2}, ["b", "a"])
with pytest.raises(AssertionError):
@@ -41,7 +45,7 @@
class CoverageTestTest(CoverageTest):
"""Test the methods in `CoverageTest`."""
- def test_file_exists(self):
+ def test_file_exists(self) -> None:
self.make_file("whoville.txt", "We are here!")
self.assert_exists("whoville.txt")
self.assert_doesnt_exist("shadow.txt")
@@ -52,7 +56,7 @@
with pytest.raises(AssertionError, match=msg):
self.assert_exists("shadow.txt")
- def test_file_count(self):
+ def test_file_count(self) -> None:
self.make_file("abcde.txt", "abcde")
self.make_file("axczz.txt", "axczz")
self.make_file("afile.txt", "afile")
@@ -83,8 +87,8 @@
with pytest.raises(AssertionError, match=msg):
self.assert_file_count("*.q", 10)
- def test_assert_recent_datetime(self):
- def now_delta(seconds):
+ def test_assert_recent_datetime(self) -> None:
+ def now_delta(seconds: int) -> datetime.datetime:
"""Make a datetime `seconds` seconds from now."""
return datetime.datetime.now() + datetime.timedelta(seconds=seconds)
@@ -104,7 +108,7 @@
with pytest.raises(AssertionError):
self.assert_recent_datetime(now_delta(1), seconds=120)
- def test_assert_warnings(self):
+ def test_assert_warnings(self) -> None:
cov = coverage.Coverage()
# Make a warning, it should catch it properly.
@@ -153,7 +157,7 @@
with self.assert_warnings(cov, ["Hello there!"]):
raise ZeroDivisionError("oops")
- def test_assert_no_warnings(self):
+ def test_assert_no_warnings(self) -> None:
cov = coverage.Coverage()
# Happy path: no warnings.
@@ -166,7 +170,7 @@
with self.assert_warnings(cov, []):
cov._warn("Watch out!")
- def test_sub_python_is_this_python(self):
+ def test_sub_python_is_this_python(self) -> None:
# Try it with a Python command.
self.set_environ('COV_FOOBAR', 'XYZZY')
self.make_file("showme.py", """\
@@ -175,10 +179,10 @@
print(os.__file__)
print(os.environ['COV_FOOBAR'])
""")
- out = self.run_command("python showme.py").splitlines()
- assert actual_path(out[0]) == actual_path(sys.executable)
- assert out[1] == os.__file__
- assert out[2] == 'XYZZY'
+ out_lines = self.run_command("python showme.py").splitlines()
+ assert actual_path(out_lines[0]) == actual_path(sys.executable)
+ assert out_lines[1] == os.__file__
+ assert out_lines[2] == 'XYZZY'
# Try it with a "coverage debug sys" command.
out = self.run_command("coverage debug sys")
@@ -192,7 +196,7 @@
_, _, environ = environ.rpartition(":")
assert environ.strip() == "COV_FOOBAR = XYZZY"
- def test_run_command_stdout_stderr(self):
+ def test_run_command_stdout_stderr(self) -> None:
# run_command should give us both stdout and stderr.
self.make_file("outputs.py", """\
import sys
@@ -203,7 +207,7 @@
assert "StdOut\n" in out
assert "StdErr\n" in out
- def test_stdout(self):
+ def test_stdout(self) -> None:
# stdout is captured.
print("This is stdout")
print("Line 2")
@@ -220,14 +224,19 @@
class Stub:
"""A stand-in for the class we're checking."""
- def __init__(self, x):
+ def __init__(self, x: int) -> None:
self.x = x
- def method(self, filename, a=17, b="hello"):
+ def method(
+ self,
+ filename: str,
+ a: int = 17,
+ b: str = "hello",
+ ) -> Tuple[int, str, int, str]:
"""The method we'll wrap, with args to be sure args work."""
return (self.x, filename, a, b)
- def test_detect_duplicate(self):
+ def test_detect_duplicate(self) -> None:
stub = self.Stub(23)
CheckUniqueFilenames.hook(stub, "method")
@@ -260,7 +269,7 @@
ARCZ_MISSING = "3-2 78 8B"
ARCZ_UNPREDICTED = "79"
- def test_check_coverage_possible(self):
+ def test_check_coverage_possible(self) -> None:
msg = r"(?s)Possible arcs differ: .*- \(6, 3\).*\+ \(6, 7\)"
with pytest.raises(AssertionError, match=msg):
self.check_coverage(
@@ -270,7 +279,7 @@
arcz_unpredicted=self.ARCZ_UNPREDICTED,
)
- def test_check_coverage_missing(self):
+ def test_check_coverage_missing(self) -> None:
msg = r"(?s)Missing arcs differ: .*- \(3, 8\).*\+ \(7, 8\)"
with pytest.raises(AssertionError, match=msg):
self.check_coverage(
@@ -280,7 +289,7 @@
arcz_unpredicted=self.ARCZ_UNPREDICTED,
)
- def test_check_coverage_unpredicted(self):
+ def test_check_coverage_unpredicted(self) -> None:
msg = r"(?s)Unpredicted arcs differ: .*- \(3, 9\).*\+ \(7, 9\)"
with pytest.raises(AssertionError, match=msg):
self.check_coverage(
@@ -301,7 +310,7 @@
("[13]", "line1\nline2\nline3\n", "line1\nline3\n"),
("X", "line1\nline2\nline3\n", ""),
])
- def test_re_lines(self, pat, text, result):
+ def test_re_lines(self, pat: str, text: str, result: str) -> None:
assert re_lines_text(pat, text) == result
assert re_lines(pat, text) == result.splitlines()
@@ -310,26 +319,26 @@
("[13]", "line1\nline2\nline3\n", "line2\n"),
("X", "line1\nline2\nline3\n", "line1\nline2\nline3\n"),
])
- def test_re_lines_inverted(self, pat, text, result):
+ def test_re_lines_inverted(self, pat: str, text: str, result: str) -> None:
assert re_lines_text(pat, text, match=False) == result
assert re_lines(pat, text, match=False) == result.splitlines()
@pytest.mark.parametrize("pat, text, result", [
("2", "line1\nline2\nline3\n", "line2"),
])
- def test_re_line(self, pat, text, result):
+ def test_re_line(self, pat: str, text: str, result: str) -> None:
assert re_line(pat, text) == result
@pytest.mark.parametrize("pat, text", [
("line", "line1\nline2\nline3\n"), # too many matches
("X", "line1\nline2\nline3\n"), # no matches
])
- def test_re_line_bad(self, pat, text):
+ def test_re_line_bad(self, pat: str, text: str) -> None:
with pytest.raises(AssertionError):
re_line(pat, text)
-def _same_python_executable(e1, e2):
+def _same_python_executable(e1: str, e2: str) -> bool:
"""Determine if `e1` and `e2` refer to the same Python executable.
Either path could include symbolic links. The two paths might not refer
@@ -356,16 +365,6 @@
return False # pragma: only failure
-def test_without_module():
- toml1 = tomlconfig.tomllib
- with without_module(tomlconfig, 'tomllib'):
- toml2 = tomlconfig.tomllib
- toml3 = tomlconfig.tomllib
-
- assert toml1 is toml3 is not None
- assert toml2 is None
-
-
class ArczTest(CoverageTest):
"""Tests of arcz/arcs helpers."""
@@ -376,7 +375,7 @@
("-11 12 2-5", [(-1, 1), (1, 2), (2, -5)]),
("-QA CB IT Z-A", [(-26, 10), (12, 11), (18, 29), (35, -10)]),
])
- def test_arcz_to_arcs(self, arcz, arcs):
+ def test_arcz_to_arcs(self, arcz: str, arcs: List[TArc]) -> None:
assert arcz_to_arcs(arcz) == arcs
@pytest.mark.parametrize("arcs, arcz_repr", [
@@ -393,45 +392,45 @@
)
),
])
- def test_arcs_to_arcz_repr(self, arcs, arcz_repr):
+ def test_arcs_to_arcz_repr(self, arcs: List[TArc], arcz_repr: str) -> None:
assert arcs_to_arcz_repr(arcs) == arcz_repr
class AssertCoverageWarningsTest(CoverageTest):
"""Tests of assert_coverage_warnings"""
- def test_one_warning(self):
+ def test_one_warning(self) -> None:
with pytest.warns(Warning) as warns:
warnings.warn("Hello there", category=CoverageWarning)
assert_coverage_warnings(warns, "Hello there")
- def test_many_warnings(self):
+ def test_many_warnings(self) -> None:
with pytest.warns(Warning) as warns:
warnings.warn("The first", category=CoverageWarning)
warnings.warn("The second", category=CoverageWarning)
warnings.warn("The third", category=CoverageWarning)
assert_coverage_warnings(warns, "The first", "The second", "The third")
- def test_wrong_type(self):
+ def test_wrong_type(self) -> None:
with pytest.warns(Warning) as warns:
warnings.warn("Not ours", category=Warning)
with pytest.raises(AssertionError):
assert_coverage_warnings(warns, "Not ours")
- def test_wrong_message(self):
+ def test_wrong_message(self) -> None:
with pytest.warns(Warning) as warns:
warnings.warn("Goodbye", category=CoverageWarning)
with pytest.raises(AssertionError):
assert_coverage_warnings(warns, "Hello there")
- def test_wrong_number_too_many(self):
+ def test_wrong_number_too_many(self) -> None:
with pytest.warns(Warning) as warns:
warnings.warn("The first", category=CoverageWarning)
warnings.warn("The second", category=CoverageWarning)
with pytest.raises(AssertionError):
assert_coverage_warnings(warns, "The first", "The second", "The third")
- def test_wrong_number_too_few(self):
+ def test_wrong_number_too_few(self) -> None:
with pytest.warns(Warning) as warns:
warnings.warn("The first", category=CoverageWarning)
warnings.warn("The second", category=CoverageWarning)
@@ -439,12 +438,12 @@
with pytest.raises(AssertionError):
assert_coverage_warnings(warns, "The first", "The second")
- def test_regex_matches(self):
+ def test_regex_matches(self) -> None:
with pytest.warns(Warning) as warns:
warnings.warn("The first", category=CoverageWarning)
assert_coverage_warnings(warns, re.compile("f?rst"))
- def test_regex_doesnt_match(self):
+ def test_regex_doesnt_match(self) -> None:
with pytest.warns(Warning) as warns:
warnings.warn("The first", category=CoverageWarning)
with pytest.raises(AssertionError):
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_venv.py python-coverage-7.2.7+dfsg1/tests/test_venv.py
--- python-coverage-6.5.0+dfsg1/tests/test_venv.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_venv.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,10 +3,14 @@
"""Tests about understanding how third-party code is installed."""
+from __future__ import annotations
+
import os
import os.path
import shutil
-import sys
+
+from pathlib import Path
+from typing import Iterator, cast
import pytest
@@ -17,7 +21,7 @@
from tests.helpers import re_lines, run_command
-def run_in_venv(cmd):
+def run_in_venv(cmd: str) -> str:
r"""Run `cmd` in the virtualenv at `venv`.
The first word of the command will be adjusted to run it from the
@@ -38,7 +42,7 @@
@pytest.fixture(scope="session", name="venv_world")
-def venv_world_fixture(tmp_path_factory):
+def venv_world_fixture(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""Create a virtualenv with a few test packages for VirtualenvTest to use.
Returns the directory containing the "venv" virtualenv.
@@ -112,8 +116,12 @@
__path__ = extend_path(__path__, __name__)
""")
make_file("bug888/app/testcov/main.py", """\
- import pkg_resources
- for entry_point in pkg_resources.iter_entry_points('plugins'):
+ try: # pragma: no cover
+ entry_points = __import__("pkg_resources").iter_entry_points('plugins')
+ except ImportError: # pragma: no cover
+ import importlib.metadata
+ entry_points = importlib.metadata.entry_points(group="plugins")
+ for entry_point in entry_points:
entry_point.load()()
""")
make_file("bug888/plugin/setup.py", """\
@@ -154,24 +162,18 @@
"coverage",
"python -m coverage",
], name="coverage_command")
-def coverage_command_fixture(request):
+def coverage_command_fixture(request: pytest.FixtureRequest) -> str:
"""Parametrized fixture to use multiple forms of "coverage" command."""
- return request.param
+ return cast(str, request.param)
-# https://bugs.python.org/issue46028
-@pytest.mark.xfail(
- (3, 11, 0, 'alpha', 4, 0) == env.PYVERSION and
- not os.path.exists(sys._base_executable),
- reason="avoid 3.11 bug: bpo46028"
-)
class VirtualenvTest(CoverageTest):
"""Tests of virtualenv considerations."""
expected_stdout = "33\n110\n198\n1.5\n"
@pytest.fixture(autouse=True)
- def in_venv_world_fixture(self, venv_world):
+ def in_venv_world_fixture(self, venv_world: Path) -> Iterator[None]:
"""For running tests inside venv_world, and cleaning up made files."""
with change_dir(venv_world):
self.make_file("myproduct.py", """\
@@ -185,7 +187,7 @@
print(sum(colorsys.rgb_to_hls(1, 0, 0)))
""")
- self.del_environ("COVERAGE_TESTING") # To avoid needing contracts installed.
+ self.del_environ("COVERAGE_TESTING") # To get realistic behavior
self.set_environ("COVERAGE_DEBUG_FILE", "debug_out.txt")
self.set_environ("COVERAGE_DEBUG", "trace")
@@ -195,13 +197,33 @@
if fname not in {"venv", "another_pkg", "bug888"}:
os.remove(fname)
- def get_trace_output(self):
+ def get_trace_output(self) -> str:
"""Get the debug output of coverage.py"""
with open("debug_out.txt") as f:
return f.read()
- def test_third_party_venv_isnt_measured(self, coverage_command):
- out = run_in_venv(coverage_command + " run --source=. myproduct.py")
+ @pytest.mark.parametrize('install_source_in_venv', [True, False])
+ def test_third_party_venv_isnt_measured(
+ self, coverage_command: str, install_source_in_venv: bool
+ ) -> None:
+ if install_source_in_venv:
+ make_file("setup.py", """\
+ import setuptools
+ setuptools.setup(
+ name="myproduct",
+ py_modules = ["myproduct"],
+ )
+ """)
+ try:
+ run_in_venv("python -m pip install .")
+ finally:
+ shutil.rmtree("build", ignore_errors=True)
+ shutil.rmtree("myproduct.egg-info", ignore_errors=True)
+ # Ensure that coverage doesn't run the non-installed module.
+ os.remove('myproduct.py')
+ out = run_in_venv(coverage_command + " run --source=.,myproduct -m myproduct")
+ else:
+ out = run_in_venv(coverage_command + " run --source=. myproduct.py")
# In particular, this warning doesn't appear:
# Already imported a file that will be measured: .../coverage/__main__.py
assert out == self.expected_stdout
@@ -215,7 +237,7 @@
)
assert re_lines(r"^Tracing .*\bmyproduct.py", debug_out)
assert re_lines(
- r"^Not tracing .*\bcolorsys.py': falls outside the --source spec",
+ r"^Not tracing .*\bcolorsys.py': (module 'colorsys' |)?falls outside the --source spec",
debug_out,
)
@@ -225,7 +247,7 @@
assert "coverage" not in out
assert "colorsys" not in out
- def test_us_in_venv_isnt_measured(self, coverage_command):
+ def test_us_in_venv_isnt_measured(self, coverage_command: str) -> None:
out = run_in_venv(coverage_command + " run --source=third myproduct.py")
assert out == self.expected_stdout
@@ -252,7 +274,7 @@
assert "coverage" not in out
assert "colorsys" not in out
- def test_venv_isnt_measured(self, coverage_command):
+ def test_venv_isnt_measured(self, coverage_command: str) -> None:
out = run_in_venv(coverage_command + " run myproduct.py")
assert out == self.expected_stdout
@@ -268,7 +290,7 @@
assert "colorsys" not in out
@pytest.mark.skipif(not env.C_TRACER, reason="Plugins are only supported with the C tracer.")
- def test_venv_with_dynamic_plugin(self, coverage_command):
+ def test_venv_with_dynamic_plugin(self, coverage_command: str) -> None:
# https://github.com/nedbat/coveragepy/issues/1150
# Django coverage plugin was incorrectly getting warnings:
# "Already imported: ... django/template/blah.py"
@@ -284,7 +306,7 @@
# Already imported a file that will be measured: ...third/render.py (already-imported)
assert out == "HTML: hello.html@1723\n"
- def test_installed_namespace_packages(self, coverage_command):
+ def test_installed_namespace_packages(self, coverage_command: str) -> None:
# https://github.com/nedbat/coveragepy/issues/1231
# When namespace packages were installed, they were considered
# third-party packages. Test that isn't still happening.
@@ -326,7 +348,7 @@
assert "fifth" in out
assert "sixth" in out
- def test_bug_888(self, coverage_command):
+ def test_bug_888(self, coverage_command: str) -> None:
out = run_in_venv(
coverage_command +
" run --source=bug888/app,bug888/plugin bug888/app/testcov/main.py"
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_version.py python-coverage-7.2.7+dfsg1/tests/test_version.py
--- python-coverage-6.5.0+dfsg1/tests/test_version.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_version.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,6 +3,8 @@
"""Tests of version.py."""
+from __future__ import annotations
+
import coverage
from coverage.version import _make_url, _make_version
@@ -14,22 +16,28 @@
run_in_temp_dir = False
- def test_version_info(self):
+ def test_version_info(self) -> None:
# Make sure we didn't screw up the version_info tuple.
assert isinstance(coverage.version_info, tuple)
assert [type(d) for d in coverage.version_info] == [int, int, int, str, int]
- assert coverage.version_info[3] in ['alpha', 'beta', 'candidate', 'final']
+ assert coverage.version_info[3] in {'alpha', 'beta', 'candidate', 'final'}
- def test_make_version(self):
- assert _make_version(4, 0, 0, 'alpha', 0) == "4.0.0a0"
+ def test_make_version(self) -> None:
+ assert _make_version(4, 0, 0, 'alpha') == "4.0.0a0"
assert _make_version(4, 0, 0, 'alpha', 1) == "4.0.0a1"
- assert _make_version(4, 0, 0, 'final', 0) == "4.0.0"
- assert _make_version(4, 1, 0, 'final', 0) == "4.1.0"
+ assert _make_version(4, 0, 0, 'final') == "4.0.0"
+ assert _make_version(4, 1, 0) == "4.1.0"
assert _make_version(4, 1, 2, 'beta', 3) == "4.1.2b3"
- assert _make_version(4, 1, 2, 'final', 0) == "4.1.2"
+ assert _make_version(4, 1, 2) == "4.1.2"
assert _make_version(5, 10, 2, 'candidate', 7) == "5.10.2rc7"
+ assert _make_version(5, 10, 2, 'candidate', 7, 3) == "5.10.2rc7.dev3"
- def test_make_url(self):
- assert _make_url(4, 0, 0, 'final', 0) == "https://coverage.readthedocs.io"
+ def test_make_url(self) -> None:
+ expected = "https://coverage.readthedocs.io/en/4.1.2"
+ assert _make_url(4, 1, 2, 'final') == expected
expected = "https://coverage.readthedocs.io/en/4.1.2b3"
assert _make_url(4, 1, 2, 'beta', 3) == expected
+ expected = "https://coverage.readthedocs.io/en/4.1.2b3.dev17"
+ assert _make_url(4, 1, 2, 'beta', 3, 17) == expected
+ expected = "https://coverage.readthedocs.io/en/4.1.2.dev17"
+ assert _make_url(4, 1, 2, 'final', 0, 17) == expected
diff -Nru python-coverage-6.5.0+dfsg1/tests/test_xml.py python-coverage-7.2.7+dfsg1/tests/test_xml.py
--- python-coverage-6.5.0+dfsg1/tests/test_xml.py 2022-09-29 16:36:36.000000000 +0000
+++ python-coverage-7.2.7+dfsg1/tests/test_xml.py 2023-05-29 19:46:30.000000000 +0000
@@ -3,14 +3,19 @@
"""Tests for XML reports from coverage.py."""
+from __future__ import annotations
+
import os
import os.path
import re
+
+from typing import Any, Dict, Iterator, Tuple, Union
from xml.etree import ElementTree
import pytest
import coverage
+from coverage import Coverage, env
from coverage.exceptions import NoDataError
from coverage.files import abs_file
from coverage.misc import import_local_file
@@ -23,7 +28,7 @@
class XmlTestHelpers(CoverageTest):
"""Methods to use from XML tests."""
- def run_doit(self):
+ def run_doit(self) -> Coverage:
"""Construct a simple sub-package."""
self.make_file("sub/__init__.py")
self.make_file("sub/doit.py", "print('doit!')")
@@ -32,7 +37,7 @@
self.start_import_stop(cov, "main")
return cov
- def make_tree(self, width, depth, curdir="."):
+ def make_tree(self, width: int, depth: int, curdir: str = ".") -> None:
"""Make a tree of packages.
Makes `width` directories, named d0 .. d{width-1}. Each directory has
@@ -44,7 +49,7 @@
if depth == 0:
return
- def here(p):
+ def here(p: str) -> str:
"""A path for `p` in our currently interesting directory."""
return os.path.join(curdir, p)
@@ -57,7 +62,11 @@
filename = here(f"f{i}.py")
self.make_file(filename, f"# {filename}\n")
- def assert_source(self, xmldom, src):
+ def assert_source(
+ self,
+ xmldom: Union[ElementTree.Element, ElementTree.ElementTree],
+ src: str,
+ ) -> None:
"""Assert that the XML has a