From fe3d110052a47ef15f9490058703ecc5026f7c09 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sat, 7 Jan 2023 16:43:58 -0500
Subject: [PATCH 01/12] build: a little more streamlining of the release
process
---
Makefile | 3 +++
howto.txt | 10 +++++-----
2 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/Makefile b/Makefile
index d906554f6..5bca7c53d 100644
--- a/Makefile
+++ b/Makefile
@@ -188,7 +188,10 @@ update_stable: ## Set the stable branch to the latest release.
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
diff --git a/howto.txt b/howto.txt
index b42f2228e..9b5893ecc 100644
--- a/howto.txt
+++ b/howto.txt
@@ -36,16 +36,17 @@
- 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
- 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.
@@ -66,7 +67,6 @@
- unopvars
- Bump version:
$ make bump_version
- $ git push
- Update readthedocs
- @ https://readthedocs.org/projects/coverage/versions/
- find the latest tag in the inactive list, edit it, make it active.
From 2c527825ac0cf394b32d773fd0ca5375dd8c031b Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sat, 7 Jan 2023 16:59:47 -0500
Subject: [PATCH 02/12] build: bump version
---
CHANGES.rst | 6 ++++++
coverage/version.py | 4 ++--
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 3932763ea..919379529 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -17,6 +17,12 @@ development at the same time, such as 4.5.x and 5.0.
.. Version 9.8.1 — 2027-07-27
.. --------------------------
+Unreleased
+----------
+
+Nothing yet.
+
+
.. _changes_7-0-4:
Version 7.0.4 — 2023-01-07
diff --git a/coverage/version.py b/coverage/version.py
index 2f70d8d7d..84eb5e26d 100644
--- a/coverage/version.py
+++ b/coverage/version.py
@@ -8,8 +8,8 @@
# version_info: same semantics as sys.version_info.
# _dev: the .devN suffix if any.
-version_info = (7, 0, 4, "final", 0)
-_dev = 0
+version_info = (7, 0, 5, "alpha", 0)
+_dev = 1
def _make_version(
From 13218037401dc30f05fd3a16a2cd52ee882fd1c4 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sat, 7 Jan 2023 21:25:42 -0500
Subject: [PATCH 03/12] mypy: test_parser.py test_phystokens.py test_process.py
test_report.py test_results.py test_setup.py
---
tests/test_parser.py | 56 ++++++++--------
tests/test_phystokens.py | 34 +++++-----
tests/test_plugins.py | 86 ++++++++++++------------
tests/test_process.py | 140 ++++++++++++++++++++-------------------
tests/test_report.py | 31 ++++++---
tests/test_results.py | 38 +++++++----
tests/test_setup.py | 14 ++--
tox.ini | 6 +-
8 files changed, 221 insertions(+), 184 deletions(-)
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 057b92446..8009ce51f 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -3,11 +3,15 @@
"""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
@@ -23,14 +27,14 @@ class PythonParserTest(CoverageTest):
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:
@@ -47,7 +51,7 @@ class Bar:
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):
@@ -63,7 +67,7 @@ def gen(input):
5:1, # list -> exit
}
- def test_try_except(self):
+ def test_try_except(self) -> None:
parser = self.parse_source("""\
try:
a = 2
@@ -79,7 +83,7 @@ def test_try_except(self):
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):
@@ -93,7 +97,7 @@ class Bar:
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
@@ -121,7 +125,7 @@ def foo():
""")
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"
@@ -133,7 +137,7 @@ def test_indentation_error(self):
1
""")
- def test_token_error(self):
+ def test_token_error(self) -> None:
msg = "Couldn't parse '' as Python source: 'EOF in multi-line string' at line 1"
with pytest.raises(NotPython, match=msg):
_ = self.parse_source("""\
@@ -141,7 +145,7 @@ def test_token_error(self):
""")
@xfail_pypy38
- def test_decorator_pragmas(self):
+ def test_decorator_pragmas(self) -> None:
parser = self.parse_source("""\
# 1
@@ -177,7 +181,7 @@ def func(x=25):
assert parser.statements == {8}
@xfail_pypy38
- def test_decorator_pragmas_with_colons(self):
+ 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("""\
@@ -197,7 +201,7 @@ def g():
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):
@@ -211,7 +215,7 @@ def __init__(self):
assert parser.raw_statements == {1, 2, 3, 5, 6, 7, 8}
assert parser.statements == {1, 2, 3}
- def test_empty_decorated_function(self):
+ def test_empty_decorated_function(self) -> None:
parser = self.parse_source("""\
def decorator(func):
return func
@@ -247,7 +251,7 @@ def bar(self):
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"
@@ -262,13 +266,13 @@ class ParserMissingArcDescriptionTest(CoverageTest):
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 +308,7 @@ def func10():
)
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,
@@ -323,7 +327,7 @@ def test_missing_arc_descriptions_for_small_callables(self):
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 +347,7 @@ def test_missing_arc_descriptions_for_exceptions(self):
)
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 +421,7 @@ def function():
)
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 +433,7 @@ def test_missing_arc_descriptions_bug460(self):
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 +454,7 @@ def test_match_case_with_default(self):
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 +463,7 @@ def parse_file(self, filename):
@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 +482,14 @@ class Bar:
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 +518,7 @@ def test_missing_line_ending(self):
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.
@@ -531,7 +535,7 @@ def test_ast_dump():
# stress_phystoken.tok has deprecation warnings, suppress them.
warnings.filterwarnings("ignore", message=r".*invalid escape sequence",)
ast_root = ast.parse(source)
- result = []
+ result: List[str] = []
ast_dump(ast_root, print=result.append)
if num_lines < 100:
continue
diff --git a/tests/test_phystokens.py b/tests/test_phystokens.py
index dae1a0ed1..5807f00d3 100644
--- a/tests/test_phystokens.py
+++ b/tests/test_phystokens.py
@@ -58,7 +58,7 @@ class PhysTokensTest(CoverageTest):
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):
@@ -71,26 +71,26 @@ def check_tokenization(self, source):
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)
@@ -99,7 +99,7 @@ def test_tokenize_real_file(self):
"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():
@@ -116,7 +116,7 @@ class SoftKeywordTest(CoverageTest):
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"]:
@@ -168,40 +168,40 @@ class SourceEncodingTest(CoverageTest):
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)
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index d407f7489..866fab871 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -27,7 +27,7 @@
class FakeConfig:
"""A fake config for use in tests."""
- def __init__(self, plugin, options):
+ def __init__(self, plugin, options) -> None:
self.plugin = plugin
self.options = options
self.asked_for = []
@@ -44,7 +44,7 @@ def get_plugin_options(self, module):
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 +62,7 @@ def coverage_init(reg, options):
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
@@ -83,7 +83,7 @@ def coverage_init(reg, options):
assert plugins[0].options == {'a': 'hello'}
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
@@ -124,11 +124,11 @@ def coverage_init(reg, options):
assert plugins[1].this_is == "me"
assert plugins[1].options == {'a': 'second'}
- 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)
- 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
@@ -141,7 +141,7 @@ def test_plugin_must_define_coverage_init(self):
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 +162,7 @@ def coverage_init(reg, options):
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 +170,7 @@ def test_missing_plugin_raises_import_error(self):
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 +179,7 @@ def test_bad_plugin_isnt_hidden(self):
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 +213,7 @@ def coverage_init(reg, options):
]
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 +239,7 @@ def coverage_init(reg, options):
]
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 +264,7 @@ def coverage_init(reg, options):
@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 +285,7 @@ class FileTracerTest(CoverageTest):
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
@@ -354,7 +354,7 @@ def lines(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*"])
@@ -379,7 +379,7 @@ def test_plugin2(self):
assert "quux_5.html" not 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*"])
@@ -400,7 +400,7 @@ def test_plugin2_with_branch(self):
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*"])
@@ -422,7 +422,7 @@ def test_plugin2_with_text_report(self):
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*"])
@@ -437,7 +437,7 @@ def test_plugin2_with_html_report(self):
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*"])
@@ -468,7 +468,7 @@ def test_plugin2_with_xml_report(self):
'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
@@ -521,7 +521,7 @@ def coverage_init(reg, options):
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
@@ -653,7 +653,7 @@ def run_bad_plugin(self, module_name, plugin_name, our_error=True, excmsg=None,
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
@@ -663,7 +663,7 @@ def coverage_init(reg, options):
""")
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):
@@ -682,7 +682,7 @@ def coverage_init(reg, options):
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):
@@ -702,7 +702,7 @@ def coverage_init(reg, options):
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):
@@ -714,7 +714,7 @@ def coverage_init(reg, options):
""")
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", """\
@@ -745,7 +745,7 @@ def coverage_init(reg, options):
""")
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):
@@ -759,7 +759,7 @@ def coverage_init(reg, options):
"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):
@@ -775,7 +775,7 @@ def coverage_init(reg, options):
""")
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):
@@ -791,7 +791,7 @@ def coverage_init(reg, options):
""")
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):
@@ -815,7 +815,7 @@ def coverage_init(reg, options):
],
)
- 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):
@@ -834,7 +834,7 @@ def coverage_init(reg, options):
""")
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):
@@ -856,7 +856,7 @@ def coverage_init(reg, options):
"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):
@@ -878,7 +878,7 @@ def coverage_init(reg, options):
"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):
@@ -900,7 +900,7 @@ def coverage_init(reg, options):
"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):
@@ -932,7 +932,7 @@ class ConfigurerPluginTest(CoverageTest):
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()
@@ -978,7 +978,7 @@ def coverage_init(reg, options):
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):
@@ -997,7 +997,7 @@ def render_bold(text):
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():
@@ -1005,7 +1005,7 @@ def doctest_html_tag():
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'
@@ -1030,7 +1030,7 @@ def run_all_functions(self, cov, suite_name): # pragma: nested
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()
@@ -1053,7 +1053,7 @@ def test_plugin_standalone(self):
data.set_query_context("test:RENDERERS")
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()
@@ -1074,7 +1074,7 @@ def test_static_context(self):
]
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,7 +1107,7 @@ def assert_context_lines(context, lines):
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()
diff --git a/tests/test_process.py b/tests/test_process.py
index 33d52923c..bdfa33164 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -3,6 +3,8 @@
"""Tests for process behavior of coverage.py."""
+from __future__ import annotations
+
import glob
import os
import os.path
@@ -11,6 +13,8 @@
import sys
import textwrap
+from typing import Any
+
import pytest
import coverage
@@ -25,7 +29,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 +39,7 @@ def test_save_on_exit(self):
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 +53,7 @@ def test_tests_dir_is_importable(self):
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 +68,7 @@ def test_coverage_run_envvar_is_in_coveragerun(self):
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 +85,7 @@ def make_b_or_c_py(self):
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 +104,7 @@ def test_append_data(self):
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 +128,7 @@ def test_append_data_with_different_file(self):
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 +142,7 @@ def test_append_can_create_a_data_file(self):
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 +186,7 @@ def test_combine_with_rc(self):
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
@@ -236,7 +240,7 @@ def test_combine_with_aliases(self):
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
@@ -253,7 +257,7 @@ def test_erase_parallel(self):
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!'
@@ -278,14 +282,14 @@ def test_missing_source_file(self):
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
@@ -315,7 +319,7 @@ def f2():
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():
@@ -337,7 +341,7 @@ def f2():
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():
@@ -354,7 +358,7 @@ def f1():
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
@@ -397,7 +401,7 @@ def main():
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", """\
@@ -418,7 +422,7 @@ def test_warnings_during_reporting(self):
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.
@@ -437,7 +441,7 @@ def test_warns_if_never_run(self):
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", """\
@@ -457,7 +461,7 @@ def run(self):
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")
@@ -473,7 +477,7 @@ def test_warning_trace_function_changed(self):
# 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.
#
@@ -527,7 +531,7 @@ def test_timid(self):
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
@@ -554,7 +558,7 @@ def f():
@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.
@@ -578,7 +582,7 @@ def test_fullcoverage(self):
# 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")
- 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 +599,7 @@ def test_lang_c(self):
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 +616,7 @@ def test_deprecation_warnings(self):
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 +647,7 @@ def foo():
)
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 +662,7 @@ def test_module_name(self):
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
@@ -669,27 +673,27 @@ def assert_tryexecfile_output(self, expected, actual):
assert '"DATA": "xyzzy"' in actual
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())
@@ -697,7 +701,7 @@ def test_coverage_run_dir_is_like_python_dir(self):
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())
@@ -705,7 +709,7 @@ def test_coverage_run_dashm_dir_no_init_is_like_python(self):
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", "")
@@ -714,7 +718,7 @@ def test_coverage_run_dashm_dir_with_init_is_like_python(self):
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
@@ -727,7 +731,7 @@ def test_coverage_run_dashm_equal_to_doubledashsource(self):
)
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,
@@ -751,7 +755,7 @@ def test_coverage_run_dashm_superset_of_doubledashsource(self):
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.
@@ -770,7 +774,7 @@ def test_coverage_run_script_imports_doubledashsource(self):
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:
@@ -780,7 +784,7 @@ def test_coverage_run_dashm_is_like_python_dashm_off_path(self):
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')")
@@ -788,7 +792,7 @@ def test_coverage_run_dashm_is_like_python_dashm_with__main__207(self):
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.
@@ -799,7 +803,7 @@ def test_coverage_zip_is_like_python(self):
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.
@@ -833,7 +837,7 @@ def test_coverage_custom_script(self):
assert "hello-xyzzy" in out
@pytest.mark.skipif(env.WINDOWS, reason="Windows can't make symlinks")
- def test_bug_862(self):
+ 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", """\
@@ -848,7 +852,7 @@ def test_bug_862(self):
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
@@ -882,7 +886,7 @@ class ExcepthookTest(CoverageTest):
# 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
@@ -912,7 +916,7 @@ def excepthook(*args):
@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
@@ -933,7 +937,7 @@ def excepthook(*args):
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
@@ -961,20 +965,20 @@ class AliasedCommandTest(CoverageTest):
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)
@@ -985,7 +989,7 @@ def test_specific_alias_works(self):
"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
@@ -996,7 +1000,7 @@ class PydocTest(CoverageTest):
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)
@@ -1008,17 +1012,17 @@ def assert_pydoc_ok(self, name, thing):
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!
@@ -1032,25 +1036,25 @@ def setUp(self):
""")
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" +
@@ -1067,7 +1071,7 @@ def test_report_99p9_is_not_ok(self):
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
@@ -1076,7 +1080,7 @@ def test_report(self):
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")
@@ -1101,12 +1105,12 @@ class YankedDirectoryTest(CoverageTest):
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')
@@ -1125,7 +1129,7 @@ def test_removing_directory_with_error(self):
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
@@ -1141,7 +1145,7 @@ def setUp(self):
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.
@@ -1165,7 +1169,7 @@ def test_subprocess_with_pth_files(self):
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]
@@ -1212,7 +1216,7 @@ class ProcessStartupWithSourceTest(CoverageTest):
@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:
@@ -1227,14 +1231,14 @@ def test_pth_and_source_work_together(self, dashm, package, source):
``--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)
diff --git a/tests/test_report.py b/tests/test_report.py
index 1e7c07624..3d87b5148 100644
--- a/tests/test_report.py
+++ b/tests/test_report.py
@@ -3,10 +3,16 @@
"""Tests for helpers in report.py"""
+from __future__ import annotations
+
+from typing import IO, Iterable, List, Optional
+
import pytest
from coverage.exceptions import CoverageException
from coverage.report import render_report
+from coverage.types import TMorf
+
from tests.coveragetest import CoverageTest
@@ -15,42 +21,45 @@ class FakeReporter:
report_type = "fake report file"
- def __init__(self, output="", error=False):
+ def __init__(self, output: str = "", error: bool = False) -> None:
self.output = output
self.error = error
- self.morfs = None
+ self.morfs: Optional[Iterable[TMorf]] = None
- def report(self, morfs, outfile):
+ def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float:
"""Fake."""
self.morfs = morfs
outfile.write(self.output)
if self.error:
raise CoverageException("You asked for it!")
+ return 17.25
class RenderReportTest(CoverageTest):
"""Tests of render_report."""
- def test_stdout(self):
+ def test_stdout(self) -> None:
fake = FakeReporter(output="Hello!\n")
- msgs = []
- render_report("-", fake, [pytest, "coverage"], msgs.append)
+ 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):
+ def test_file(self) -> None:
fake = FakeReporter(output="Gréètings!\n")
- msgs = []
- render_report("output.txt", fake, [], msgs.append)
+ 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"]
- def test_exception(self):
+ def test_exception(self) -> None:
fake = FakeReporter(error=True)
- msgs = []
+ msgs: List[str] = []
with pytest.raises(CoverageException, match="You asked for it!"):
render_report("output.txt", fake, [], msgs.append)
assert self.stdout() == ""
diff --git a/tests/test_results.py b/tests/test_results.py
index 41f3dc40a..f2a5ae83f 100644
--- a/tests/test_results.py
+++ b/tests/test_results.py
@@ -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 @@ class NumbersTest(CoverageTest):
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 @@ def test_addition(self):
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 @@ def test_sum(self):
(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 @@ def test_pc_covered_str(self, kwargs, res):
(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 @@ def test_display_covered(self, prec, pc, res):
(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 @@ def test_covered_ratio(self):
(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 @@ def test_should_fail_under_invalid_value():
([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 @@ def test_format_lines(statements, lines, result):
"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 --git a/tests/test_setup.py b/tests/test_setup.py
index 5468e3bf1..a7a97d1fe 100644
--- a/tests/test_setup.py
+++ b/tests/test_setup.py
@@ -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 @@ class SetupPyTest(CoverageTest):
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 @@ def test_metadata(self):
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 --git a/tox.ini b/tox.ini
index 8722241ca..cf0d09d29 100644
--- a/tox.ini
+++ b/tox.ini
@@ -105,8 +105,10 @@ setenv =
T2=tests/test_annotate.py tests/test_api.py tests/test_arcs.py tests/test_cmdline.py tests/test_collector.py tests/test_concurrency.py
T3=tests/test_config.py tests/test_context.py tests/test_coverage.py tests/test_data.py tests/test_debug.py tests/test_execfile.py
T4=tests/test_filereporter.py tests/test_files.py tests/test_goldtest.py tests/test_html.py tests/test_json.py tests/test_lcov.py
- T5=tests/test_misc.py tests/test_mixins.py tests/test_numbits.py tests/test_oddball.py tests/test_python.py tests/test_summary.py tests/test_xml.py
- TYPEABLE={env:C1} {env:C2} {env:C3} {env:C4} {env:C5} {env:C6} {env:T1} {env:T2} {env:T3} {env:T4} {env:T5}
+ T5=tests/test_misc.py tests/test_mixins.py tests/test_numbits.py tests/test_oddball.py tests/test_parser.py tests/test_phystokens.py
+ T6=tests/test_process.py tests/test_python.py tests/test_report.py tests/test_results.py tests/test_setup.py tests/test_summary.py tests/test_xml.py
+ # not done yet: test_plugins.py
+ TYPEABLE={env:C1} {env:C2} {env:C3} {env:C4} {env:C5} {env:C6} {env:T1} {env:T2} {env:T3} {env:T4} {env:T5} {env:T6}
commands =
# PYVERSIONS
From 65aad086ccccea33f0ff9535c9612e1b4b6712ce Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sat, 7 Jan 2023 21:38:12 -0500
Subject: [PATCH 04/12] mypy: test_testing.py test_version.py
---
tests/test_testing.py | 82 ++++++++++++++++++++++++-------------------
tests/test_version.py | 8 +++--
tox.ini | 7 ++--
3 files changed, 55 insertions(+), 42 deletions(-)
diff --git a/tests/test_testing.py b/tests/test_testing.py
index d5447c45a..7e875618e 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -3,17 +3,22 @@
"""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.exceptions import CoverageWarning
from coverage.files import actual_path
+from coverage.types import TArc
from tests.coveragetest import CoverageTest
from tests.helpers import (
@@ -22,13 +27,13 @@
)
-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):
@@ -40,7 +45,7 @@ def test_assert_count_equal():
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")
@@ -51,7 +56,7 @@ def test_file_exists(self):
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")
@@ -82,8 +87,8 @@ def test_file_count(self):
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)
@@ -103,7 +108,7 @@ def now_delta(seconds):
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.
@@ -152,7 +157,7 @@ def test_assert_warnings(self):
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.
@@ -165,7 +170,7 @@ def test_assert_no_warnings(self):
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", """\
@@ -174,10 +179,10 @@ def test_sub_python_is_this_python(self):
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")
@@ -191,7 +196,7 @@ def test_sub_python_is_this_python(self):
_, _, 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
@@ -202,7 +207,7 @@ def test_run_command_stdout_stderr(self):
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")
@@ -219,14 +224,19 @@ class CheckUniqueFilenamesTest(CoverageTest):
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")
@@ -259,7 +269,7 @@ def oops(x):
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(
@@ -269,7 +279,7 @@ def test_check_coverage_possible(self):
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(
@@ -279,7 +289,7 @@ def test_check_coverage_missing(self):
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(
@@ -300,7 +310,7 @@ class ReLinesTest(CoverageTest):
("[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()
@@ -309,26 +319,26 @@ def test_re_lines(self, pat, text, result):
("[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
@@ -365,7 +375,7 @@ class ArczTest(CoverageTest):
("-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", [
@@ -382,45 +392,45 @@ def test_arcz_to_arcs(self, arcz, arcs):
)
),
])
- 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)
@@ -428,12 +438,12 @@ def test_wrong_number_too_few(self):
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 --git a/tests/test_version.py b/tests/test_version.py
index ce6c705ac..9efa228ab 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -3,6 +3,8 @@
"""Tests of version.py."""
+from __future__ import annotations
+
import coverage
from coverage.version import _make_url, _make_version
@@ -14,13 +16,13 @@ class VersionTest(CoverageTest):
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'}
- def test_make_version(self):
+ 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') == "4.0.0"
@@ -30,7 +32,7 @@ def test_make_version(self):
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):
+ def test_make_url(self) -> None:
assert _make_url(4, 0, 0, 'final') == "https://coverage.readthedocs.io"
expected = "https://coverage.readthedocs.io/en/4.1.2b3"
assert _make_url(4, 1, 2, 'beta', 3) == expected
diff --git a/tox.ini b/tox.ini
index cf0d09d29..bf5f40bad 100644
--- a/tox.ini
+++ b/tox.ini
@@ -106,9 +106,10 @@ setenv =
T3=tests/test_config.py tests/test_context.py tests/test_coverage.py tests/test_data.py tests/test_debug.py tests/test_execfile.py
T4=tests/test_filereporter.py tests/test_files.py tests/test_goldtest.py tests/test_html.py tests/test_json.py tests/test_lcov.py
T5=tests/test_misc.py tests/test_mixins.py tests/test_numbits.py tests/test_oddball.py tests/test_parser.py tests/test_phystokens.py
- T6=tests/test_process.py tests/test_python.py tests/test_report.py tests/test_results.py tests/test_setup.py tests/test_summary.py tests/test_xml.py
- # not done yet: test_plugins.py
- TYPEABLE={env:C1} {env:C2} {env:C3} {env:C4} {env:C5} {env:C6} {env:T1} {env:T2} {env:T3} {env:T4} {env:T5} {env:T6}
+ T6=tests/test_process.py tests/test_python.py tests/test_report.py tests/test_results.py tests/test_setup.py
+ T7=tests/test_summary.py tests/test_testing.py tests/test_version.py tests/test_xml.py
+ # not done yet: test_plugins.py test_templite.py test_venv.py
+ TYPEABLE={env:C1} {env:C2} {env:C3} {env:C4} {env:C5} {env:C6} {env:T1} {env:T2} {env:T3} {env:T4} {env:T5} {env:T6} {env:T7}
commands =
# PYVERSIONS
From 08564c09144b2223be808f49b001c8856966bd46 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sat, 7 Jan 2023 22:54:05 -0500
Subject: [PATCH 05/12] mypy: templite.py test_templite.py
---
coverage/templite.py | 52 ++++++++++++++---------
tests/test_templite.py | 96 +++++++++++++++++++++---------------------
tox.ini | 11 +++--
3 files changed, 87 insertions(+), 72 deletions(-)
diff --git a/coverage/templite.py b/coverage/templite.py
index 29596d770..897a58f95 100644
--- a/coverage/templite.py
+++ b/coverage/templite.py
@@ -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 TempliteValueError(ValueError):
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 @@ def add_line(self, line):
"""
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 @@ def add_section(self):
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
@@ -111,7 +117,7 @@ class Templite:
})
"""
- 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 @@ def __init__(self, text, *contexts):
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 @@ def __init__(self, text, *contexts):
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])
@@ -232,9 +238,15 @@ def flush_output():
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 @@ def _expr_code(self, expr):
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 @@ def _variable(self, name, vars_set):
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 @@ def render(self, context=None):
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 --git a/tests/test_templite.py b/tests/test_templite.py
index d2e98479b..e34f71692 100644
--- a/tests/test_templite.py
+++ b/tests/test_templite.py
@@ -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 @@ def try_render(self, text, ctx=None, result=None):
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 @@ def test_pipes(self):
# 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 @@ def test_reusability(self):
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 @@ def test_loops(self):
"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 @@ def rev(l):
"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 @@ def test_multiple_loops(self):
"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 @@ def test_comments(self):
{'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 @@ def test_if(self):
"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 @@ def getit(self):
"@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 @@ def test_loop_if(self):
"X!"
)
- def test_nested_loops(self):
+ def test_nested_loops(self) -> None:
self.try_render(
"@" +
"{% for n in nums %}" +
@@ -238,7 +238,7 @@ def test_nested_loops(self):
"@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 @@ def test_whitespace_handling(self):
)
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 @@ def test_eat_whitespace(self):
"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 @@ def test_exception_during_evaluation(self):
"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 @@ def test_bad_names(self):
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 @@ def test_malformed_for(self):
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 @@ def test_bad_nesting(self):
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 --git a/tox.ini b/tox.ini
index bf5f40bad..0308b5a37 100644
--- a/tox.ini
+++ b/tox.ini
@@ -100,16 +100,19 @@ setenv =
C3=coverage/data.py coverage/debug.py coverage/disposition.py coverage/env.py coverage/exceptions.py
C4=coverage/files.py coverage/inorout.py coverage/jsonreport.py coverage/lcovreport.py coverage/misc.py coverage/multiproc.py coverage/numbits.py
C5=coverage/parser.py coverage/phystokens.py coverage/plugin.py coverage/plugin_support.py coverage/python.py
- C6=coverage/report.py coverage/results.py coverage/sqldata.py coverage/summary.py coverage/tomlconfig.py coverage/types.py coverage/version.py coverage/xmlreport.py
+ C6=coverage/report.py coverage/results.py coverage/sqldata.py coverage/summary.py
+ C7=coverage/templite.py coverage/tomlconfig.py coverage/types.py coverage/version.py coverage/xmlreport.py
+ TYPEABLE_C={env:C1} {env:C2} {env:C3} {env:C4} {env:C5} {env:C6} {env:C7}
T1=tests/conftest.py tests/coveragetest.py tests/goldtest.py tests/helpers.py tests/mixins.py tests/osinfo.py
T2=tests/test_annotate.py tests/test_api.py tests/test_arcs.py tests/test_cmdline.py tests/test_collector.py tests/test_concurrency.py
T3=tests/test_config.py tests/test_context.py tests/test_coverage.py tests/test_data.py tests/test_debug.py tests/test_execfile.py
T4=tests/test_filereporter.py tests/test_files.py tests/test_goldtest.py tests/test_html.py tests/test_json.py tests/test_lcov.py
T5=tests/test_misc.py tests/test_mixins.py tests/test_numbits.py tests/test_oddball.py tests/test_parser.py tests/test_phystokens.py
T6=tests/test_process.py tests/test_python.py tests/test_report.py tests/test_results.py tests/test_setup.py
- T7=tests/test_summary.py tests/test_testing.py tests/test_version.py tests/test_xml.py
- # not done yet: test_plugins.py test_templite.py test_venv.py
- TYPEABLE={env:C1} {env:C2} {env:C3} {env:C4} {env:C5} {env:C6} {env:T1} {env:T2} {env:T3} {env:T4} {env:T5} {env:T6} {env:T7}
+ T7=tests/test_summary.py tests/test_templite.py tests/test_testing.py tests/test_version.py tests/test_xml.py
+ # not done yet: test_plugins.py test_venv.py
+ TYPEABLE_T={env:T1} {env:T2} {env:T3} {env:T4} {env:T5} {env:T6} {env:T7}
+ TYPEABLE={env:TYPEABLE_C} {env:TYPEABLE_T}
commands =
# PYVERSIONS
From 8fef6f057c377879720c4c9d994e9651362a49b9 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sat, 7 Jan 2023 23:08:48 -0500
Subject: [PATCH 06/12] mypy: test_venv.py
---
tests/helpers.py | 3 ++-
tests/test_venv.py | 31 ++++++++++++++++++-------------
tox.ini | 4 ++--
3 files changed, 22 insertions(+), 16 deletions(-)
diff --git a/tests/helpers.py b/tests/helpers.py
index 1c4b2f96e..83d0cb0c7 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -15,6 +15,7 @@
import textwrap
import warnings
+from pathlib import Path
from typing import (
Any, Callable, Iterable, Iterator, List, Optional, Set, Tuple, Type,
TypeVar, Union, cast,
@@ -267,7 +268,7 @@ def arcs_to_arcz_repr(arcs: Optional[Iterable[TArc]]) -> str:
@contextlib.contextmanager
-def change_dir(new_dir: str) -> Iterator[None]:
+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
diff --git a/tests/test_venv.py b/tests/test_venv.py
index c7436c4e4..eb4ed5c0a 100644
--- a/tests/test_venv.py
+++ b/tests/test_venv.py
@@ -3,10 +3,15 @@
"""Tests about understanding how third-party code is installed."""
+from __future__ import annotations
+
import os
import os.path
import shutil
+from pathlib import Path
+from typing import Iterator, cast
+
import pytest
from coverage import env
@@ -16,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
@@ -37,13 +42,13 @@ def run_in_venv(cmd):
@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.
"""
- venv_world = tmp_path_factory.mktemp("venv_world")
+ venv_world = cast(Path, tmp_path_factory.mktemp("venv_world"))
with change_dir(venv_world):
# Create a virtualenv.
run_command("python -m venv venv")
@@ -153,9 +158,9 @@ def testp():
"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)
class VirtualenvTest(CoverageTest):
@@ -164,7 +169,7 @@ class VirtualenvTest(CoverageTest):
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", """\
@@ -188,12 +193,12 @@ def in_venv_world_fixture(self, venv_world):
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):
+ def test_third_party_venv_isnt_measured(self, coverage_command: str) -> None:
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
@@ -218,7 +223,7 @@ def test_third_party_venv_isnt_measured(self, coverage_command):
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
@@ -245,7 +250,7 @@ def test_us_in_venv_isnt_measured(self, coverage_command):
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
@@ -261,7 +266,7 @@ def test_venv_isnt_measured(self, coverage_command):
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"
@@ -277,7 +282,7 @@ def test_venv_with_dynamic_plugin(self, coverage_command):
# 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.
@@ -319,7 +324,7 @@ def test_installed_namespace_packages(self, coverage_command):
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 --git a/tox.ini b/tox.ini
index 0308b5a37..431b714c1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -109,8 +109,8 @@ setenv =
T4=tests/test_filereporter.py tests/test_files.py tests/test_goldtest.py tests/test_html.py tests/test_json.py tests/test_lcov.py
T5=tests/test_misc.py tests/test_mixins.py tests/test_numbits.py tests/test_oddball.py tests/test_parser.py tests/test_phystokens.py
T6=tests/test_process.py tests/test_python.py tests/test_report.py tests/test_results.py tests/test_setup.py
- T7=tests/test_summary.py tests/test_templite.py tests/test_testing.py tests/test_version.py tests/test_xml.py
- # not done yet: test_plugins.py test_venv.py
+ T7=tests/test_summary.py tests/test_templite.py tests/test_testing.py tests/test_venv.py tests/test_version.py tests/test_xml.py
+ # not done yet: test_plugins.py
TYPEABLE_T={env:T1} {env:T2} {env:T3} {env:T4} {env:T5} {env:T6} {env:T7}
TYPEABLE={env:TYPEABLE_C} {env:TYPEABLE_T}
From 880a64afd24aae34eff2781f568d8ac9807d2ecc Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Mon, 9 Jan 2023 17:42:53 -0500
Subject: [PATCH 07/12] fix: isolate user code from coverage.py internal code
flags. #1524
---
CHANGES.rst | 6 +++++-
coverage/execfile.py | 2 +-
coverage/parser.py | 2 +-
lab/genpy.py | 2 +-
lab/parser.py | 2 +-
lab/show_pyc.py | 2 +-
setup.py | 2 +-
tests/test_cmdline.py | 2 +-
tests/test_summary.py | 16 ++++++++++++++++
9 files changed, 28 insertions(+), 8 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 919379529..765695f85 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -20,7 +20,11 @@ development at the same time, such as 4.5.x and 5.0.
Unreleased
----------
-Nothing yet.
+- Fix: On Python 3.7, a file with type annotations but no ``from __future__
+ import annotations`` would be missing statements in the coverage report. This
+ is now fixed, closing `issue 1524`_.
+
+.. _issue 1524: https://github.com/nedbat/coveragepy/issues/1524
.. _changes_7-0-4:
diff --git a/coverage/execfile.py b/coverage/execfile.py
index f0f4f171d..d26da65bb 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -275,7 +275,7 @@ def make_code_from_py(filename):
except (OSError, NoSource) as exc:
raise NoSource(f"No file to run: '{filename}'") from exc
- return compile(source, filename, "exec")
+ return compile(source, filename, "exec", dont_inherit=True)
def make_code_from_pyc(filename):
diff --git a/coverage/parser.py b/coverage/parser.py
index 37d747674..b8ddb5015 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -385,7 +385,7 @@ def __init__(
else:
assert filename is not None
try:
- self.code = compile(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" % (
diff --git a/lab/genpy.py b/lab/genpy.py
index f968c9163..f88e70ca8 100644
--- a/lab/genpy.py
+++ b/lab/genpy.py
@@ -231,7 +231,7 @@ def show_a_bunch():
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 --git a/lab/parser.py b/lab/parser.py
index ebd4e7f3a..c7687bda6 100644
--- a/lab/parser.py
+++ b/lab/parser.py
@@ -177,7 +177,7 @@ def all_code_objects(code):
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 --git a/lab/show_pyc.py b/lab/show_pyc.py
index e346930a5..1bd98ec64 100644
--- a/lab/show_pyc.py
+++ b/lab/show_pyc.py
@@ -48,7 +48,7 @@ def show_py_file(fname):
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 --git a/setup.py b/setup.py
index c30907f92..dd7676013 100644
--- a/setup.py
+++ b/setup.py
@@ -59,7 +59,7 @@ def better_set_verbosity(v):
# 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:
readme_text = readme.read()
diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py
index 6caac307f..c517d39d3 100644
--- a/tests/test_cmdline.py
+++ b/tests/test_cmdline.py
@@ -142,7 +142,7 @@ def cmd_executes(
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
diff --git a/tests/test_summary.py b/tests/test_summary.py
index 3109e90f2..f532a7b1f 100644
--- a/tests/test_summary.py
+++ b/tests/test_summary.py
@@ -849,6 +849,22 @@ def missing(x, y):
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."""
From b893cb31f43a9b598990c462cfaa2c8c66779153 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 10 Jan 2023 07:00:11 -0500
Subject: [PATCH 08/12] mypy: execfile.py
---
coverage/execfile.py | 52 ++++++++++++++++++++++++++++++--------------
tox.ini | 2 +-
2 files changed, 37 insertions(+), 17 deletions(-)
diff --git a/coverage/execfile.py b/coverage/execfile.py
index d26da65bb..66cf931c8 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -12,7 +12,10 @@
import os
import struct
import sys
-import types
+
+from importlib.machinery import ModuleSpec
+from types import CodeType, ModuleType
+from typing import Any, List, Optional, Tuple
from coverage import env
from coverage.exceptions import CoverageException, _ExceptionDuringRun, NoCode, NoSource
@@ -30,11 +33,13 @@ class DummyLoader:
Currently only implements the .fullname attribute
"""
- def __init__(self, fullname, *_args):
+ def __init__(self, fullname: str, *_args: Any) -> None:
self.fullname = fullname
-def find_module(modulename):
+def find_module(
+ modulename: str,
+) -> Tuple[Optional[str], str, ModuleSpec]:
"""Find the module named `modulename`.
Returns the file path of the module, the name of the enclosing
@@ -68,18 +73,23 @@ class PyRunner:
This is meant to emulate real Python execution as closely as possible.
"""
- def __init__(self, args, as_module=False):
+ def __init__(self, args: List[str], as_module: bool = False) -> None:
self.args = args
self.as_module = as_module
self.arg0 = args[0]
- self.package = self.modulename = self.pathname = self.loader = self.spec = None
+ self.package: Optional[str] = None
+ self.modulename: Optional[str] = None
+ self.pathname: Optional[str] = None
+ self.loader: Optional[DummyLoader] = None
+ self.spec: Optional[ModuleSpec] = None
- def prepare(self):
+ def prepare(self) -> None:
"""Set sys.path properly.
This needs to happen before any importing, and without importing anything.
"""
+ path0: Optional[str]
if self.as_module:
path0 = os.getcwd()
elif os.path.isdir(self.arg0):
@@ -113,7 +123,7 @@ def prepare(self):
if path0 is not None:
sys.path[0] = python_reported_file(path0)
- def _prepare2(self):
+ def _prepare2(self) -> None:
"""Do more preparation to run Python code.
Includes finding the module to run and adjusting sys.argv[0].
@@ -126,6 +136,7 @@ def _prepare2(self):
if self.spec is not None:
self.modulename = self.spec.name
self.loader = DummyLoader(self.modulename)
+ assert pathname is not None
self.pathname = os.path.abspath(pathname)
self.args[0] = self.arg0 = self.pathname
elif os.path.isdir(self.arg0):
@@ -155,13 +166,13 @@ def _prepare2(self):
self.arg0 = python_reported_file(self.arg0)
- def run(self):
+ def run(self) -> None:
"""Run the Python code!"""
self._prepare2()
# Create a module to serve as __main__
- main_mod = types.ModuleType('__main__')
+ main_mod = ModuleType('__main__')
from_pyc = self.arg0.endswith((".pyc", ".pyo"))
main_mod.__file__ = self.arg0
@@ -169,11 +180,11 @@ def run(self):
main_mod.__file__ = main_mod.__file__[:-1]
if self.package is not None:
main_mod.__package__ = self.package
- main_mod.__loader__ = self.loader
+ main_mod.__loader__ = self.loader # type: ignore[assignment]
if self.spec is not None:
main_mod.__spec__ = self.spec
- main_mod.__builtins__ = sys.modules['builtins']
+ main_mod.__builtins__ = sys.modules['builtins'] # type: ignore[attr-defined]
sys.modules['__main__'] = main_mod
@@ -209,6 +220,9 @@ def run(self):
# so that the coverage.py code doesn't appear in the final printed
# traceback.
typ, err, tb = sys.exc_info()
+ assert typ is not None
+ assert err is not None
+ assert tb is not None
# PyPy3 weirdness. If I don't access __context__, then somehow it
# is non-None when the exception is reported at the upper layer,
@@ -218,6 +232,7 @@ def run(self):
# Call the excepthook.
try:
+ assert err.__traceback__ is not None
err.__traceback__ = err.__traceback__.tb_next
sys.excepthook(typ, err, tb.tb_next)
except SystemExit: # pylint: disable=try-except-raise
@@ -227,7 +242,11 @@ def run(self):
# shenanigans is kind of involved.
sys.stderr.write("Error in sys.excepthook:\n")
typ2, err2, tb2 = sys.exc_info()
+ assert typ2 is not None
+ assert err2 is not None
+ assert tb2 is not None
err2.__suppress_context__ = True
+ assert err2.__traceback__ is not None
err2.__traceback__ = err2.__traceback__.tb_next
sys.__excepthook__(typ2, err2, tb2.tb_next)
sys.stderr.write("\nOriginal exception was:\n")
@@ -238,7 +257,7 @@ def run(self):
os.chdir(cwd)
-def run_python_module(args):
+def run_python_module(args: List[str]) -> None:
"""Run a Python module, as though with ``python -m name args...``.
`args` is the argument array to present as sys.argv, including the first
@@ -252,7 +271,7 @@ def run_python_module(args):
runner.run()
-def run_python_file(args):
+def run_python_file(args: List[str]) -> None:
"""Run a Python file as if it were the main program on the command line.
`args` is the argument array to present as sys.argv, including the first
@@ -267,7 +286,7 @@ def run_python_file(args):
runner.run()
-def make_code_from_py(filename):
+def make_code_from_py(filename: str) -> CodeType:
"""Get source from `filename` and make a code object of it."""
# Open the source file.
try:
@@ -275,10 +294,10 @@ def make_code_from_py(filename):
except (OSError, NoSource) as exc:
raise NoSource(f"No file to run: '{filename}'") from exc
- return compile(source, filename, "exec", dont_inherit=True)
+ return compile(source, filename, "exec", dont_inherit=True) # type: ignore[no-any-return]
-def make_code_from_pyc(filename):
+def make_code_from_pyc(filename: str) -> CodeType:
"""Get a code object from a .pyc file."""
try:
fpyc = open(filename, "rb")
@@ -303,5 +322,6 @@ def make_code_from_pyc(filename):
# The rest of the file is the code object we want.
code = marshal.load(fpyc)
+ assert isinstance(code, CodeType)
return code
diff --git a/tox.ini b/tox.ini
index 431b714c1..ecece7ff5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -97,7 +97,7 @@ setenv =
{[testenv]setenv}
C1=coverage/__init__.py coverage/__main__.py coverage/annotate.py coverage/bytecode.py
C2=coverage/cmdline.py coverage/collector.py coverage/config.py coverage/context.py coverage/control.py
- C3=coverage/data.py coverage/debug.py coverage/disposition.py coverage/env.py coverage/exceptions.py
+ C3=coverage/data.py coverage/debug.py coverage/disposition.py coverage/env.py coverage/exceptions.py coverage/execfile.py
C4=coverage/files.py coverage/inorout.py coverage/jsonreport.py coverage/lcovreport.py coverage/misc.py coverage/multiproc.py coverage/numbits.py
C5=coverage/parser.py coverage/phystokens.py coverage/plugin.py coverage/plugin_support.py coverage/python.py
C6=coverage/report.py coverage/results.py coverage/sqldata.py coverage/summary.py
From c9d473b05a1cdcd9d04185ee4fb4b86e1e5f08e3 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 10 Jan 2023 13:33:06 -0500
Subject: [PATCH 09/12] mypy: install pytest alongside mypy to get its types
---
requirements/mypy.in | 3 ++
requirements/mypy.pip | 89 ++++++++++++++++++++++++++++++++++++++++++-
tests/mixins.py | 8 ++--
tests/test_oddball.py | 1 +
tests/test_venv.py | 2 +-
5 files changed, 96 insertions(+), 7 deletions(-)
diff --git a/requirements/mypy.in b/requirements/mypy.in
index 50828014b..871c589c3 100644
--- a/requirements/mypy.in
+++ b/requirements/mypy.in
@@ -3,4 +3,7 @@
-c pins.pip
+# So that we have pytest types.
+-r pytest.pip
+
mypy
diff --git a/requirements/mypy.pip b/requirements/mypy.pip
index 6ea7438c5..d36cf2d4b 100644
--- a/requirements/mypy.pip
+++ b/requirements/mypy.pip
@@ -4,6 +4,51 @@
#
# make upgrade
#
+attrs==22.2.0 \
+ --hash=sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836 \
+ --hash=sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99
+ # via
+ # -r requirements/pytest.pip
+ # hypothesis
+ # pytest
+colorama==0.4.6 \
+ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
+ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
+ # via -r requirements/pytest.pip
+exceptiongroup==1.1.0 \
+ --hash=sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e \
+ --hash=sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23
+ # via
+ # -r requirements/pytest.pip
+ # hypothesis
+ # pytest
+execnet==1.9.0 \
+ --hash=sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5 \
+ --hash=sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142
+ # via
+ # -r requirements/pytest.pip
+ # pytest-xdist
+flaky==3.7.0 \
+ --hash=sha256:3ad100780721a1911f57a165809b7ea265a7863305acb66708220820caf8aa0d \
+ --hash=sha256:d6eda73cab5ae7364504b7c44670f70abed9e75f77dd116352f662817592ec9c
+ # via -r requirements/pytest.pip
+hypothesis==6.62.0 \
+ --hash=sha256:76f1141e8237f6dd0780a171bec5d6aec873208ccc27b5f9753d4cccd8904272 \
+ --hash=sha256:e250da77878460f74b53039493a7a18d6fc137b0b77791b382b6a0f4ada9144e
+ # via -r requirements/pytest.pip
+importlib-metadata==6.0.0 \
+ --hash=sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad \
+ --hash=sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d
+ # via
+ # -r requirements/pytest.pip
+ # pluggy
+ # pytest
+iniconfig==2.0.0 \
+ --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
+ --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
+ # via
+ # -r requirements/pytest.pip
+ # pytest
mypy==0.991 \
--hash=sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d \
--hash=sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6 \
@@ -40,10 +85,41 @@ mypy-extensions==0.4.3 \
--hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \
--hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8
# via mypy
+packaging==23.0 \
+ --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
+ --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97
+ # via
+ # -r requirements/pytest.pip
+ # pytest
+pluggy==1.0.0 \
+ --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \
+ --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3
+ # via
+ # -r requirements/pytest.pip
+ # pytest
+pytest==7.2.0 \
+ --hash=sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71 \
+ --hash=sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59
+ # via
+ # -r requirements/pytest.pip
+ # pytest-xdist
+pytest-xdist==3.1.0 \
+ --hash=sha256:40fdb8f3544921c5dfcd486ac080ce22870e71d82ced6d2e78fa97c2addd480c \
+ --hash=sha256:70a76f191d8a1d2d6be69fc440cdf85f3e4c03c08b520fd5dc5d338d6cf07d89
+ # via -r requirements/pytest.pip
+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
- # via mypy
+ # via
+ # -r requirements/pytest.pip
+ # mypy
+ # pytest
typed-ast==1.5.4 \
--hash=sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2 \
--hash=sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1 \
@@ -73,4 +149,13 @@ typed-ast==1.5.4 \
typing-extensions==4.4.0 \
--hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \
--hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e
- # via mypy
+ # via
+ # -r requirements/pytest.pip
+ # importlib-metadata
+ # mypy
+zipp==3.11.0 \
+ --hash=sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa \
+ --hash=sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766
+ # via
+ # -r requirements/pytest.pip
+ # importlib-metadata
diff --git a/tests/mixins.py b/tests/mixins.py
index d207f7798..c8f79d675 100644
--- a/tests/mixins.py
+++ b/tests/mixins.py
@@ -14,7 +14,7 @@
import os.path
import sys
-from typing import Any, Callable, Iterable, Iterator, Optional, Tuple
+from typing import Any, Callable, Iterable, Iterator, Optional, Tuple, cast
import pytest
@@ -138,12 +138,12 @@ def _capcapsys(self, capsys: pytest.CaptureFixture[str]) -> None:
def stdouterr(self) -> Tuple[str, str]:
"""Returns (out, err), two strings for stdout and stderr."""
- return self.capsys.readouterr() # type: ignore[no-any-return]
+ return cast(Tuple[str, str], self.capsys.readouterr())
def stdout(self) -> str:
"""Returns a string, the captured stdout."""
- return self.capsys.readouterr().out # type: ignore[no-any-return]
+ return self.capsys.readouterr().out
def stderr(self) -> str:
"""Returns a string, the captured stderr."""
- return self.capsys.readouterr().err # type: ignore[no-any-return]
+ return self.capsys.readouterr().err
diff --git a/tests/test_oddball.py b/tests/test_oddball.py
index a44beae48..23e3ce9d0 100644
--- a/tests/test_oddball.py
+++ b/tests/test_oddball.py
@@ -209,6 +209,7 @@ def test_dropping_none(self) -> None: # pragma: not covered
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():
diff --git a/tests/test_venv.py b/tests/test_venv.py
index eb4ed5c0a..de7ebbe18 100644
--- a/tests/test_venv.py
+++ b/tests/test_venv.py
@@ -48,7 +48,7 @@ def venv_world_fixture(tmp_path_factory: pytest.TempPathFactory) -> Path:
Returns the directory containing the "venv" virtualenv.
"""
- venv_world = cast(Path, tmp_path_factory.mktemp("venv_world"))
+ venv_world = tmp_path_factory.mktemp("venv_world")
with change_dir(venv_world):
# Create a virtualenv.
run_command("python -m venv venv")
From c55dffe5284dc99f7a6764f2f45ab82140733d93 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 10 Jan 2023 13:49:05 -0500
Subject: [PATCH 10/12] mypy: exclude fullcoverage/encodings.py
---
coverage/fullcoverage/encodings.py | 3 +++
pyproject.toml | 4 ++++
tox.ini | 1 +
3 files changed, 8 insertions(+)
diff --git a/coverage/fullcoverage/encodings.py b/coverage/fullcoverage/encodings.py
index b88418663..73bd5646e 100644
--- a/coverage/fullcoverage/encodings.py
+++ b/coverage/fullcoverage/encodings.py
@@ -14,6 +14,9 @@
a problem with coverage.py - that it starts too late to trace the coverage of
many of the most fundamental modules in the Standard Library.
+DO NOT import other modules into here, it will interfere with the goal of this
+code executing before all imports. This is why this file isn't type-checked.
+
"""
import sys
diff --git a/pyproject.toml b/pyproject.toml
index d2d2100f5..561ff9f77 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -23,3 +23,7 @@ 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.
+ )"""
diff --git a/tox.ini b/tox.ini
index ecece7ff5..5a5bfc90d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -102,6 +102,7 @@ setenv =
C5=coverage/parser.py coverage/phystokens.py coverage/plugin.py coverage/plugin_support.py coverage/python.py
C6=coverage/report.py coverage/results.py coverage/sqldata.py coverage/summary.py
C7=coverage/templite.py coverage/tomlconfig.py coverage/types.py coverage/version.py coverage/xmlreport.py
+ # not done yet: html.py pytracer.py
TYPEABLE_C={env:C1} {env:C2} {env:C3} {env:C4} {env:C5} {env:C6} {env:C7}
T1=tests/conftest.py tests/coveragetest.py tests/goldtest.py tests/helpers.py tests/mixins.py tests/osinfo.py
T2=tests/test_annotate.py tests/test_api.py tests/test_arcs.py tests/test_cmdline.py tests/test_collector.py tests/test_concurrency.py
From ba217636bb6ef81be2f14d85f8c1980212576183 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 10 Jan 2023 18:16:53 -0500
Subject: [PATCH 11/12] docs: prep for 7.0.5
---
CHANGES.rst | 6 ++++--
coverage/version.py | 4 ++--
doc/conf.py | 6 +++---
3 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 765695f85..628999113 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -17,8 +17,10 @@ development at the same time, such as 4.5.x and 5.0.
.. Version 9.8.1 — 2027-07-27
.. --------------------------
-Unreleased
-----------
+.. _changes_7-0-5:
+
+Version 7.0.5 — 2023-01-10
+--------------------------
- Fix: On Python 3.7, a file with type annotations but no ``from __future__
import annotations`` would be missing statements in the coverage report. This
diff --git a/coverage/version.py b/coverage/version.py
index 84eb5e26d..b20b5568f 100644
--- a/coverage/version.py
+++ b/coverage/version.py
@@ -8,8 +8,8 @@
# version_info: same semantics as sys.version_info.
# _dev: the .devN suffix if any.
-version_info = (7, 0, 5, "alpha", 0)
-_dev = 1
+version_info = (7, 0, 5, "final", 0)
+_dev = 0
def _make_version(
diff --git a/doc/conf.py b/doc/conf.py
index 2d4be09d8..b321144e9 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -65,11 +65,11 @@
# @@@ editable
copyright = "2009–2023, Ned Batchelder" # pylint: disable=redefined-builtin
# The short X.Y.Z version.
-version = "7.0.4"
+version = "7.0.5"
# The full version, including alpha/beta/rc tags.
-release = "7.0.4"
+release = "7.0.5"
# The date of release, in "monthname day, year" format.
-release_date = "January 7, 2023"
+release_date = "January 10, 2023"
# @@@ end
rst_epilog = """
From 97642c8e81f62a445ac4a6921088d999253e11c8 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 10 Jan 2023 18:17:28 -0500
Subject: [PATCH 12/12] docs: sample html for 7.0.5
---
doc/sample_html/d_7b071bdc2a35fa80___init___py.html | 8 ++++----
doc/sample_html/d_7b071bdc2a35fa80___main___py.html | 8 ++++----
doc/sample_html/d_7b071bdc2a35fa80_backward_py.html | 8 ++++----
doc/sample_html/d_7b071bdc2a35fa80_cogapp_py.html | 8 ++++----
doc/sample_html/d_7b071bdc2a35fa80_makefiles_py.html | 8 ++++----
doc/sample_html/d_7b071bdc2a35fa80_test_cogapp_py.html | 8 ++++----
doc/sample_html/d_7b071bdc2a35fa80_test_makefiles_py.html | 8 ++++----
.../d_7b071bdc2a35fa80_test_whiteutils_py.html | 8 ++++----
doc/sample_html/d_7b071bdc2a35fa80_whiteutils_py.html | 8 ++++----
doc/sample_html/index.html | 8 ++++----
doc/sample_html/status.json | 2 +-
11 files changed, 41 insertions(+), 41 deletions(-)
diff --git a/doc/sample_html/d_7b071bdc2a35fa80___init___py.html b/doc/sample_html/d_7b071bdc2a35fa80___init___py.html
index 3ad063011..18bdc7adb 100644
--- a/doc/sample_html/d_7b071bdc2a35fa80___init___py.html
+++ b/doc/sample_html/d_7b071bdc2a35fa80___init___py.html
@@ -66,8 +66,8 @@