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

^ index     » next       - coverage.py v7.0.4, - created at 2023-01-07 15:38 -0500 + coverage.py v7.0.5, + created at 2023-01-10 18:17 -0500