diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2022-12-31 14:53:21 -0500 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2022-12-31 14:53:21 -0500 |
commit | bf73b37080c3c6deec969a555b45b70ee6727b13 (patch) | |
tree | 98f6c9c20d89afcc4d0c2396a155229fbd802290 | |
parent | ee1e4150529e55cd860fc3628b820d3a2ed471de (diff) | |
download | python-coveragepy-git-bf73b37080c3c6deec969a555b45b70ee6727b13.tar.gz |
mypy: check tests/goldtest.py, tests/test_html.py
-rw-r--r-- | coverage/config.py | 6 | ||||
-rw-r--r-- | coverage/control.py | 12 | ||||
-rw-r--r-- | coverage/html.py | 75 | ||||
-rw-r--r-- | coverage/types.py | 2 | ||||
-rw-r--r-- | tests/goldtest.py | 44 | ||||
-rw-r--r-- | tests/test_html.py | 163 | ||||
-rw-r--r-- | tox.ini | 2 |
7 files changed, 178 insertions, 126 deletions
diff --git a/coverage/config.py b/coverage/config.py index aae6065b..7e4d07db 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -226,10 +226,10 @@ class CoverageConfig(TConfigurable): self.sort = None # Defaults for [html] - self.extra_css = None + self.extra_css: Optional[str] = None self.html_dir = "htmlcov" - self.html_skip_covered = None - self.html_skip_empty = None + self.html_skip_covered: Optional[bool] = None + self.html_skip_empty: Optional[bool] = None self.html_title = "Coverage report" self.show_contexts = False diff --git a/coverage/control.py b/coverage/control.py index 6bbc17c7..be47ec37 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -866,7 +866,7 @@ class Coverage(TConfigurable): analysis.missing_formatted(), ) - def _analyze(self, it): + def _analyze(self, it) -> Analysis: """Analyze a single morf or code unit. Returns an `Analysis` object. @@ -949,7 +949,7 @@ class Coverage(TConfigurable): precision=None, sort=None, output_format=None, - ): + ) -> float: """Write a textual summary report to `file`. Each module in `morfs` is listed, with counts of statements, executed @@ -1070,7 +1070,7 @@ class Coverage(TConfigurable): contexts=None, skip_empty=None, precision=None, - ): + ) -> float: """Generate an HTML report. The HTML is written to `directory`. The file "index.html" is the @@ -1123,7 +1123,7 @@ class Coverage(TConfigurable): include=None, contexts=None, skip_empty=None, - ): + ) -> float: """Generate an XML report of coverage results. The report is compatible with Cobertura reports. @@ -1158,7 +1158,7 @@ class Coverage(TConfigurable): contexts=None, pretty_print=None, show_contexts=None, - ): + ) -> float: """Generate a JSON report of coverage results. Each module in `morfs` is included in the report. `outfile` is the @@ -1192,7 +1192,7 @@ class Coverage(TConfigurable): omit=None, include=None, contexts=None, - ): + ) -> float: """Generate an LCOV report of coverage results. Each module in 'morfs' is included in the report. 'outfile' is the diff --git a/coverage/html.py b/coverage/html.py index 21b5189e..3fcecc5d 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -3,12 +3,16 @@ """HTML reporting for coverage.py.""" +from __future__ import annotations + import datetime import json import os import re import shutil -import types + +from dataclasses import dataclass +from typing import Iterable, List, Optional, TYPE_CHECKING import coverage from coverage.data import add_data_to_hash @@ -17,13 +21,18 @@ from coverage.files import flat_rootname from coverage.misc import ensure_dir, file_be_gone, Hasher, isolate_module, format_local_datetime from coverage.misc import human_sorted, plural from coverage.report import get_analysis_to_report -from coverage.results import Numbers +from coverage.results import Analysis, Numbers from coverage.templite import Templite +from coverage.types import TLineNo, TMorf + +if TYPE_CHECKING: + from coverage import Coverage + from coverage.plugins import FileReporter os = isolate_module(os) -def data_filename(fname): +def data_filename(fname: str) -> str: """Return the path to an "htmlfiles" data file of ours. """ static_dir = os.path.join(os.path.dirname(__file__), "htmlfiles") @@ -31,25 +40,47 @@ def data_filename(fname): return static_filename -def read_data(fname): +def read_data(fname: str) -> str: """Return the contents of a data file of ours.""" with open(data_filename(fname)) as data_file: return data_file.read() -def write_html(fname, html): +def write_html(fname: str, html: str) -> None: """Write `html` to `fname`, properly encoded.""" html = re.sub(r"(\A\s+)|(\s+$)", "", html, flags=re.MULTILINE) + "\n" with open(fname, "wb") as fout: fout.write(html.encode('ascii', 'xmlcharrefreplace')) +@dataclass +class LineData: + """The data for each source line of HTML output.""" + tokens: str + number: TLineNo + category: str + statement: bool + contexts: List[str] + contexts_label: str + context_list: List[str] + short_annotations: List[str] + long_annotations: List[str] + + +@dataclass +class FileData: + """The data for each source file of HTML output.""" + relative_filename: str + nums: Numbers + lines: List[LineData] + + class HtmlDataGeneration: """Generate structured data to be turned into HTML reports.""" EMPTY = "(empty)" - def __init__(self, cov): + def __init__(self, cov: Coverage) -> None: self.coverage = cov self.config = self.coverage.config data = self.coverage.get_data() @@ -59,7 +90,7 @@ class HtmlDataGeneration: self.coverage._warn("No contexts were measured") data.set_query_contexts(self.config.report_contexts) - def data_for_file(self, fr, analysis): + def data_for_file(self, fr: FileReporter, analysis: Analysis) -> FileData: """Produce the data needed for one file's report.""" if self.has_arcs: missing_branch_arcs = analysis.missing_branch_arcs() @@ -72,7 +103,7 @@ class HtmlDataGeneration: for lineno, tokens in enumerate(fr.source_token_lines(), start=1): # Figure out how to mark this line. - category = None + category = "" short_annotations = [] long_annotations = [] @@ -86,13 +117,14 @@ class HtmlDataGeneration: if b < 0: short_annotations.append("exit") else: - short_annotations.append(b) + short_annotations.append(str(b)) long_annotations.append(fr.missing_arc_description(lineno, b, arcs_executed)) elif lineno in analysis.statements: category = 'run' - contexts = contexts_label = None - context_list = None + contexts = [] + contexts_label = "" + context_list = [] if category and self.config.show_contexts: contexts = human_sorted(c or self.EMPTY for c in contexts_by_lineno.get(lineno, ())) if contexts == [self.EMPTY]: @@ -101,7 +133,7 @@ class HtmlDataGeneration: contexts_label = f"{len(contexts)} ctx" context_list = contexts - lines.append(types.SimpleNamespace( + lines.append(LineData( tokens=tokens, number=lineno, category=category, @@ -113,7 +145,7 @@ class HtmlDataGeneration: long_annotations=long_annotations, )) - file_data = types.SimpleNamespace( + file_data = FileData( relative_filename=fr.relative_filename(), nums=analysis.numbers, lines=lines, @@ -124,7 +156,7 @@ class HtmlDataGeneration: class FileToReport: """A file we're considering reporting.""" - def __init__(self, fr, analysis): + def __init__(self, fr: FileReporter, analysis: Analysis) -> None: self.fr = fr self.analysis = analysis self.rootname = flat_rootname(fr.relative_filename()) @@ -144,7 +176,7 @@ class HtmlReporter: "favicon_32.png", ] - def __init__(self, cov): + def __init__(self, cov: Coverage) -> None: self.coverage = cov self.config = self.coverage.config self.directory = self.config.html_dir @@ -160,6 +192,7 @@ class HtmlReporter: title = self.config.html_title + self.extra_css: Optional[str] if self.config.extra_css: self.extra_css = os.path.basename(self.config.extra_css) else: @@ -204,7 +237,7 @@ class HtmlReporter: self.pyfile_html_source = read_data("pyfile.html") self.source_tmpl = Templite(self.pyfile_html_source, self.template_globals) - def report(self, morfs): + def report(self, morfs: Iterable[TMorf]) -> float: """Generate an HTML report for `morfs`. `morfs` is a list of modules or file names. @@ -254,13 +287,13 @@ class HtmlReporter: self.make_local_static_report_files() return self.totals.n_statements and self.totals.pc_covered - def make_directory(self): + def make_directory(self) -> None: """Make sure our htmlcov directory exists.""" ensure_dir(self.directory) if not os.listdir(self.directory): self.directory_was_empty = True - def make_local_static_report_files(self): + def make_local_static_report_files(self) -> None: """Make local instances of static files for HTML report.""" # The files we provide must always be copied. for static in self.STATIC_FILES: @@ -439,12 +472,12 @@ class IncrementalChecker: self.directory = directory self.reset() - def reset(self): + def reset(self) -> None: """Initialize to empty. Causes all files to be reported.""" self.globals = '' self.files = {} - def read(self): + def read(self) -> None: """Read the information we stored last time.""" usable = False try: @@ -469,7 +502,7 @@ class IncrementalChecker: else: self.reset() - def write(self): + def write(self) -> None: """Write the current status.""" status_file = os.path.join(self.directory, self.STATUS_FILE) files = {} diff --git a/coverage/types.py b/coverage/types.py index 6e69fc09..c9d05958 100644 --- a/coverage/types.py +++ b/coverage/types.py @@ -25,7 +25,7 @@ TCovKwargs = Any ## Configuration # One value read from a config file. -TConfigValue = Union[str, List[str]] +TConfigValue = Union[bool, str, List[str]] # An entire config section, mapping option names to values. TConfigSection = Dict[str, TConfigValue] diff --git a/tests/goldtest.py b/tests/goldtest.py index bb88b1e4..16b40999 100644 --- a/tests/goldtest.py +++ b/tests/goldtest.py @@ -11,19 +11,24 @@ import os.path import re import xml.etree.ElementTree +from typing import Iterable, List, Optional, Tuple + from tests.coveragetest import TESTS_DIR from tests.helpers import os_sep -def gold_path(path): +def gold_path(path: str) -> str: """Get a path to a gold file for comparison.""" return os.path.join(TESTS_DIR, "gold", path) def compare( - expected_dir, actual_dir, file_pattern=None, - actual_extra=False, scrubs=None, - ): + expected_dir: str, + actual_dir: str, + file_pattern: Optional[str]=None, + actual_extra: bool=False, + scrubs: Optional[List[Tuple[str, str]]]=None, +) -> None: """Compare files matching `file_pattern` in `expected_dir` and `actual_dir`. `actual_extra` true means `actual_dir` can have extra files in it @@ -41,11 +46,11 @@ def compare( assert os_sep("/gold/") in expected_dir dc = filecmp.dircmp(expected_dir, actual_dir) - diff_files = fnmatch_list(dc.diff_files, file_pattern) - expected_only = fnmatch_list(dc.left_only, file_pattern) - actual_only = fnmatch_list(dc.right_only, file_pattern) + diff_files = _fnmatch_list(dc.diff_files, file_pattern) + expected_only = _fnmatch_list(dc.left_only, file_pattern) + actual_only = _fnmatch_list(dc.right_only, file_pattern) - def save_mismatch(f): + def save_mismatch(f: str) -> None: """Save a mismatched result to tests/actual.""" save_path = expected_dir.replace(os_sep("/gold/"), os_sep("/actual/")) os.makedirs(save_path, exist_ok=True) @@ -75,10 +80,10 @@ def compare( actual = scrub(actual, scrubs) if expected != actual: text_diff.append(f'{expected_file} != {actual_file}') - expected = expected.splitlines() - actual = actual.splitlines() + expected_lines = expected.splitlines() + actual_lines = actual.splitlines() print(f":::: diff '{expected_file}' and '{actual_file}'") - print("\n".join(difflib.Differ().compare(expected, actual))) + print("\n".join(difflib.Differ().compare(expected_lines, actual_lines))) print(f":::: end diff '{expected_file}' and '{actual_file}'") save_mismatch(f) @@ -93,7 +98,7 @@ def compare( assert not actual_only, f"Files in {actual_dir} only: {actual_only}" -def contains(filename, *strlist): +def contains(filename: str, *strlist: str) -> None: """Check that the file contains all of a list of strings. An assert will be raised if one of the arguments in `strlist` is @@ -107,7 +112,7 @@ def contains(filename, *strlist): assert s in text, f"Missing content in {filename}: {s!r}" -def contains_rx(filename, *rxlist): +def contains_rx(filename: str, *rxlist: str) -> None: """Check that the file has lines that re.search all of the regexes. An assert will be raised if one of the regexes in `rxlist` doesn't match @@ -123,7 +128,7 @@ def contains_rx(filename, *rxlist): ) -def contains_any(filename, *strlist): +def contains_any(filename: str, *strlist: str) -> None: """Check that the file contains at least one of a list of strings. An assert will be raised if none of the arguments in `strlist` is in @@ -140,7 +145,7 @@ def contains_any(filename, *strlist): assert False, f"Missing content in {filename}: {strlist[0]!r} [1 of {len(strlist)}]" -def doesnt_contain(filename, *strlist): +def doesnt_contain(filename: str, *strlist: str) -> None: """Check that the file contains none of a list of strings. An assert will be raised if any of the strings in `strlist` appears in @@ -156,16 +161,15 @@ def doesnt_contain(filename, *strlist): # Helpers -def canonicalize_xml(xtext): +def canonicalize_xml(xtext: str) -> str: """Canonicalize some XML text.""" root = xml.etree.ElementTree.fromstring(xtext) for node in root.iter(): node.attrib = dict(sorted(node.items())) - xtext = xml.etree.ElementTree.tostring(root) - return xtext.decode("utf-8") + return xml.etree.ElementTree.tostring(root).decode("utf-8") -def fnmatch_list(files, file_pattern): +def _fnmatch_list(files: List[str], file_pattern: Optional[str]) -> List[str]: """Filter the list of `files` to only those that match `file_pattern`. If `file_pattern` is None, then return the entire list of files. Returns a list of the filtered files. @@ -175,7 +179,7 @@ def fnmatch_list(files, file_pattern): return files -def scrub(strdata, scrubs): +def scrub(strdata: str, scrubs: Iterable[Tuple[str, str]]) -> str: """Scrub uninteresting data from the payload in `strdata`. `scrubs` is a list of (find, replace) pairs of regexes that are used on `strdata`. A string is returned. diff --git a/tests/test_html.py b/tests/test_html.py index 00416769..2475c873 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -13,14 +13,17 @@ import re import sys from unittest import mock +from typing import Any, Dict, IO, List, Optional, Set, Tuple + import pytest import coverage -from coverage import env +from coverage import env, Coverage from coverage.exceptions import NoDataError, NotPython, NoSource from coverage.files import abs_file, flat_rootname import coverage.html from coverage.report import get_analysis_to_report +from coverage.types import TLineNo, TMorf from tests.coveragetest import CoverageTest, TESTS_DIR from tests.goldtest import gold_path @@ -31,7 +34,7 @@ from tests.helpers import assert_coverage_warnings, change_dir class HtmlTestHelpers(CoverageTest): """Methods that help with HTML tests.""" - def create_initial_files(self): + def create_initial_files(self) -> None: """Create the source files we need to run these tests.""" self.make_file("main_file.py", """\ import helper1, helper2 @@ -48,7 +51,11 @@ class HtmlTestHelpers(CoverageTest): print("x is %d" % x) """) - def run_coverage(self, covargs=None, htmlargs=None): + def run_coverage( + self, + covargs: Optional[Dict[str, Any]]=None, + htmlargs: Optional[Dict[str, Any]]=None, + ) -> float: """Run coverage.py on main_file.py, and create an HTML report.""" self.clean_local_file_imports() cov = coverage.Coverage(**(covargs or {})) @@ -57,14 +64,14 @@ class HtmlTestHelpers(CoverageTest): self.assert_valid_hrefs() return ret - def get_html_report_content(self, module): + def get_html_report_content(self, module: str) -> str: """Return the content of the HTML report for `module`.""" filename = flat_rootname(module) + ".html" filename = os.path.join("htmlcov", filename) with open(filename) as f: return f.read() - def get_html_index_content(self): + def get_html_index_content(self) -> str: """Return the content of index.html. Time stamps are replaced with a placeholder so that clocks don't matter. @@ -84,12 +91,12 @@ class HtmlTestHelpers(CoverageTest): ) return index - def assert_correct_timestamp(self, html): + def assert_correct_timestamp(self, html: str) -> None: """Extract the time stamp from `html`, and assert it is recent.""" timestamp_pat = r"created at (\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})" m = re.search(timestamp_pat, html) assert m, "Didn't find a time stamp!" - timestamp = datetime.datetime(*map(int, m.groups())) + timestamp = datetime.datetime(*[int(v) for v in m.groups()]) # type: ignore[arg-type] # The time stamp only records the minute, so the delta could be from # 12:00 to 12:01:59, or two minutes. self.assert_recent_datetime( @@ -98,7 +105,7 @@ class HtmlTestHelpers(CoverageTest): msg=f"Time stamp is wrong: {timestamp}", ) - def assert_valid_hrefs(self): + def assert_valid_hrefs(self) -> None: """Assert that the hrefs in htmlcov/*.html to see the references are valid. Doesn't check external links (those with a protocol). @@ -124,10 +131,10 @@ class HtmlTestHelpers(CoverageTest): class FileWriteTracker: """A fake object to track how `open` is used to write files.""" - def __init__(self, written): + def __init__(self, written: Set[str]) -> None: self.written = written - def open(self, filename, mode="r"): + def open(self, filename: str, mode: str="r") -> IO[str]: """Be just like `open`, but write written file names to `self.written`.""" if mode.startswith("w"): self.written.add(filename.replace('\\', '/')) @@ -137,7 +144,7 @@ class FileWriteTracker: class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): """Tests of the HTML delta speed-ups.""" - def setUp(self): + def setUp(self) -> None: super().setUp() # At least one of our tests monkey-patches the version of coverage.py, @@ -145,9 +152,13 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): self.real_coverage_version = coverage.__version__ self.addCleanup(setattr, coverage, "__version__", self.real_coverage_version) - self.files_written = None + self.files_written: Set[str] - def run_coverage(self, covargs=None, htmlargs=None): + def run_coverage( + self, + covargs: Optional[Dict[str, Any]]=None, + htmlargs: Optional[Dict[str, Any]]=None, + ) -> float: """Run coverage in-process for the delta tests. For the delta tests, we always want `source=.` and we want to track @@ -162,7 +173,7 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): with mock.patch("coverage.html.open", mock_open): return super().run_coverage(covargs=covargs, htmlargs=htmlargs) - def assert_htmlcov_files_exist(self): + def assert_htmlcov_files_exist(self) -> None: """Assert that all the expected htmlcov files exist.""" self.assert_exists("htmlcov/index.html") self.assert_exists("htmlcov/main_file_py.html") @@ -172,13 +183,13 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): self.assert_exists("htmlcov/coverage_html.js") self.assert_exists("htmlcov/.gitignore") - def test_html_created(self): + def test_html_created(self) -> None: # Test basic HTML generation: files should be created. self.create_initial_files() self.run_coverage() self.assert_htmlcov_files_exist() - def test_html_delta_from_source_change(self): + def test_html_delta_from_source_change(self) -> None: # HTML generation can create only the files that have changed. # In this case, helper1 changes because its source is different. self.create_initial_files() @@ -205,7 +216,7 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): index2 = self.get_html_index_content() assert index1 == index2 - def test_html_delta_from_coverage_change(self): + def test_html_delta_from_coverage_change(self) -> None: # HTML generation can create only the files that have changed. # In this case, helper1 changes because its coverage is different. self.create_initial_files() @@ -228,7 +239,7 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): assert "htmlcov/helper2_py.html" not in self.files_written assert "htmlcov/main_file_py.html" in self.files_written - def test_html_delta_from_settings_change(self): + def test_html_delta_from_settings_change(self) -> None: # HTML generation can create only the files that have changed. # In this case, everything changes because the coverage.py settings # have changed. @@ -248,7 +259,7 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): index2 = self.get_html_index_content() assert index1 == index2 - def test_html_delta_from_coverage_version_change(self): + def test_html_delta_from_coverage_version_change(self) -> None: # HTML generation can create only the files that have changed. # In this case, everything changes because the coverage.py version has # changed. @@ -272,7 +283,7 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): fixed_index2 = index2.replace("XYZZY", self.real_coverage_version) assert index1 == fixed_index2 - def test_file_becomes_100(self): + def test_file_becomes_100(self) -> None: self.create_initial_files() self.run_coverage() @@ -289,7 +300,7 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): # The 100% file, skipped, shouldn't be here. self.assert_doesnt_exist("htmlcov/helper1_py.html") - def test_status_format_change(self): + def test_status_format_change(self) -> None: self.create_initial_files() self.run_coverage() @@ -310,14 +321,14 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): assert "htmlcov/helper2_py.html" in self.files_written assert "htmlcov/main_file_py.html" in self.files_written - def test_dont_overwrite_gitignore(self): + def test_dont_overwrite_gitignore(self) -> None: self.create_initial_files() self.make_file("htmlcov/.gitignore", "# ignore nothing") self.run_coverage() with open("htmlcov/.gitignore") as fgi: assert fgi.read() == "# ignore nothing" - def test_dont_write_gitignore_into_existing_directory(self): + def test_dont_write_gitignore_into_existing_directory(self) -> None: self.create_initial_files() self.make_file("htmlcov/README", "My files: don't touch!") self.run_coverage() @@ -328,14 +339,14 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): class HtmlTitleTest(HtmlTestHelpers, CoverageTest): """Tests of the HTML title support.""" - def test_default_title(self): + def test_default_title(self) -> None: self.create_initial_files() self.run_coverage() index = self.get_html_index_content() assert "<title>Coverage report</title>" in index assert "<h1>Coverage report:" in index - def test_title_set_in_config_file(self): + def test_title_set_in_config_file(self) -> None: self.create_initial_files() self.make_file(".coveragerc", "[html]\ntitle = Metrics & stuff!\n") self.run_coverage() @@ -343,7 +354,7 @@ class HtmlTitleTest(HtmlTestHelpers, CoverageTest): assert "<title>Metrics & stuff!</title>" in index assert "<h1>Metrics & stuff!:" in index - def test_non_ascii_title_set_in_config_file(self): + def test_non_ascii_title_set_in_config_file(self) -> None: self.create_initial_files() self.make_file(".coveragerc", "[html]\ntitle = «ταБЬℓσ» numbers") self.run_coverage() @@ -351,7 +362,7 @@ class HtmlTitleTest(HtmlTestHelpers, CoverageTest): assert "<title>«ταБЬℓσ» numbers" in index assert "<h1>«ταБЬℓσ» numbers" in index - def test_title_set_in_args(self): + def test_title_set_in_args(self) -> None: self.create_initial_files() self.make_file(".coveragerc", "[html]\ntitle = Good title\n") self.run_coverage(htmlargs=dict(title="«ταБЬℓσ» & stüff!")) @@ -367,7 +378,7 @@ class HtmlTitleTest(HtmlTestHelpers, CoverageTest): class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): """Test the behavior when measuring unparsable files.""" - def test_dotpy_not_python(self): + def test_dotpy_not_python(self) -> None: self.make_file("main.py", "import innocuous") self.make_file("innocuous.py", "a = 1") cov = coverage.Coverage() @@ -377,7 +388,7 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): with pytest.raises(NotPython, match=msg): cov.html_report() - def test_dotpy_not_python_ignored(self): + def test_dotpy_not_python_ignored(self) -> None: self.make_file("main.py", "import innocuous") self.make_file("innocuous.py", "a = 2") cov = coverage.Coverage() @@ -394,7 +405,7 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): # This would be better as a glob, if the HTML layout changes: self.assert_doesnt_exist("htmlcov/innocuous.html") - def test_dothtml_not_python(self): + def test_dothtml_not_python(self) -> None: # Run an "HTML" file self.make_file("innocuous.html", "a = 3") self.make_data_file(lines={abs_file("innocuous.html"): [1]}) @@ -405,7 +416,7 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): with pytest.raises(NoDataError, match="No data to report."): cov.html_report() - def test_execed_liar_ignored(self): + def test_execed_liar_ignored(self) -> None: # Jinja2 sets __file__ to be a non-Python file, and then execs code. # If that file contains non-Python code, a TokenError shouldn't # have been raised when writing the HTML report. @@ -417,7 +428,7 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): cov.html_report() self.assert_exists("htmlcov/index.html") - def test_execed_liar_ignored_indentation_error(self): + def test_execed_liar_ignored_indentation_error(self) -> None: # Jinja2 sets __file__ to be a non-Python file, and then execs code. # If that file contains untokenizable code, we shouldn't get an # exception. @@ -430,7 +441,7 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): cov.html_report() self.assert_exists("htmlcov/index.html") - def test_decode_error(self): + def test_decode_error(self) -> None: # https://github.com/nedbat/coveragepy/issues/351 # imp.load_module won't load a file with an undecodable character # in a comment, though Python will run them. So we'll change the @@ -459,7 +470,7 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): expected = "# Isn't this great?�!" assert expected in html_report - def test_formfeeds(self): + def test_formfeeds(self) -> None: # https://github.com/nedbat/coveragepy/issues/360 self.make_file("formfeed.py", "line_one = 1\n\f\nline_two = 2\n") cov = coverage.Coverage() @@ -469,7 +480,7 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): formfeed_html = self.get_html_report_content("formfeed.py") assert "line_two" in formfeed_html - def test_splitlines_special_chars(self): + def test_splitlines_special_chars(self) -> None: # https://github.com/nedbat/coveragepy/issues/1512 # See https://docs.python.org/3/library/stdtypes.html#str.splitlines for # the characters splitlines treats specially that readlines does not. @@ -505,7 +516,7 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): class HtmlTest(HtmlTestHelpers, CoverageTest): """Moar HTML tests.""" - def test_missing_source_file_incorrect_message(self): + def test_missing_source_file_incorrect_message(self) -> None: # https://github.com/nedbat/coveragepy/issues/60 self.make_file("thefile.py", "import sub.another\n") self.make_file("sub/__init__.py", "") @@ -520,7 +531,7 @@ class HtmlTest(HtmlTestHelpers, CoverageTest): with pytest.raises(NoSource, match=msg): cov.html_report() - def test_extensionless_file_collides_with_extension(self): + def test_extensionless_file_collides_with_extension(self) -> None: # It used to be that "program" and "program.py" would both be reported # to "program.html". Now they are not. # https://github.com/nedbat/coveragepy/issues/69 @@ -537,7 +548,7 @@ class HtmlTest(HtmlTestHelpers, CoverageTest): self.assert_exists("htmlcov/program.html") self.assert_exists("htmlcov/program_py.html") - def test_has_date_stamp_in_files(self): + def test_has_date_stamp_in_files(self) -> None: self.create_initial_files() self.run_coverage() @@ -546,7 +557,7 @@ class HtmlTest(HtmlTestHelpers, CoverageTest): with open("htmlcov/main_file_py.html") as f: self.assert_correct_timestamp(f.read()) - def test_reporting_on_unmeasured_file(self): + def test_reporting_on_unmeasured_file(self) -> None: # It should be ok to ask for an HTML report on a file that wasn't even # measured at all. https://github.com/nedbat/coveragepy/issues/403 self.create_initial_files() @@ -555,7 +566,7 @@ class HtmlTest(HtmlTestHelpers, CoverageTest): self.assert_exists("htmlcov/index.html") self.assert_exists("htmlcov/other_py.html") - def make_main_and_not_covered(self): + def make_main_and_not_covered(self) -> None: """Helper to create files for skip_covered scenarios.""" self.make_file("main_file.py", """ import not_covered @@ -569,14 +580,14 @@ class HtmlTest(HtmlTestHelpers, CoverageTest): print("n") """) - def test_report_skip_covered(self): + def test_report_skip_covered(self) -> None: self.make_main_and_not_covered() self.run_coverage(htmlargs=dict(skip_covered=True)) self.assert_exists("htmlcov/index.html") self.assert_doesnt_exist("htmlcov/main_file_py.html") self.assert_exists("htmlcov/not_covered_py.html") - def test_html_skip_covered(self): + def test_html_skip_covered(self) -> None: self.make_main_and_not_covered() self.make_file(".coveragerc", "[html]\nskip_covered = True") self.run_coverage() @@ -586,14 +597,14 @@ class HtmlTest(HtmlTestHelpers, CoverageTest): index = self.get_html_index_content() assert "1 file skipped due to complete coverage." in index - def test_report_skip_covered_branches(self): + def test_report_skip_covered_branches(self) -> None: self.make_main_and_not_covered() self.run_coverage(covargs=dict(branch=True), htmlargs=dict(skip_covered=True)) self.assert_exists("htmlcov/index.html") self.assert_doesnt_exist("htmlcov/main_file_py.html") self.assert_exists("htmlcov/not_covered_py.html") - def test_report_skip_covered_100(self): + def test_report_skip_covered_100(self) -> None: self.make_file("main_file.py", """ def normal(): print("z") @@ -603,7 +614,7 @@ class HtmlTest(HtmlTestHelpers, CoverageTest): assert res == 100.0 self.assert_doesnt_exist("htmlcov/main_file_py.html") - def make_init_and_main(self): + def make_init_and_main(self) -> None: """Helper to create files for skip_empty scenarios.""" self.make_file("submodule/__init__.py", "") self.make_file("main_file.py", """ @@ -614,7 +625,7 @@ class HtmlTest(HtmlTestHelpers, CoverageTest): normal() """) - def test_report_skip_empty(self): + def test_report_skip_empty(self) -> None: self.make_init_and_main() self.run_coverage(htmlargs=dict(skip_empty=True)) self.assert_exists("htmlcov/index.html") @@ -623,7 +634,7 @@ class HtmlTest(HtmlTestHelpers, CoverageTest): index = self.get_html_index_content() assert "1 empty file skipped." in index - def test_html_skip_empty(self): + def test_html_skip_empty(self) -> None: self.make_init_and_main() self.make_file(".coveragerc", "[html]\nskip_empty = True") self.run_coverage() @@ -632,7 +643,7 @@ class HtmlTest(HtmlTestHelpers, CoverageTest): self.assert_doesnt_exist("htmlcov/submodule___init___py.html") -def filepath_to_regex(path): +def filepath_to_regex(path: str) -> str: """Create a regex for scrubbing a file path.""" regex = re.escape(path) # If there's a backslash, let it match either slash. @@ -642,7 +653,11 @@ def filepath_to_regex(path): return regex -def compare_html(expected, actual, extra_scrubs=None): +def compare_html( + expected: str, + actual: str, + extra_scrubs: Optional[List[Tuple[str, str]]]=None, +) -> None: """Specialized compare function for our HTML files.""" __tracebackhide__ = True # pytest, please don't show me this function. scrubs = [ @@ -671,7 +686,7 @@ def compare_html(expected, actual, extra_scrubs=None): class HtmlGoldTest(CoverageTest): """Tests of HTML reporting that use gold files.""" - def test_a(self): + def test_a(self) -> None: self.make_file("a.py", """\ if 1 < 2: # Needed a < to look at HTML entities. @@ -700,7 +715,7 @@ class HtmlGoldTest(CoverageTest): '<td class="right" data-ratio="2 3">67%</td>', ) - def test_b_branch(self): + def test_b_branch(self) -> None: self.make_file("b.py", """\ def one(x): # This will be a branch that misses the else. @@ -765,7 +780,7 @@ class HtmlGoldTest(CoverageTest): '<td class="right" data-ratio="16 23">70%</td>', ) - def test_bom(self): + def test_bom(self) -> None: self.make_file("bom.py", bytes=b"""\ \xef\xbb\xbf# A Python source file in utf-8, with BOM. math = "3\xc3\x974 = 12, \xc3\xb72 = 6\xc2\xb10" @@ -798,7 +813,7 @@ else: '<span class="str">"3×4 = 12, ÷2 = 6±0"</span>', ) - def test_isolatin1(self): + def test_isolatin1(self) -> None: self.make_file("isolatin1.py", bytes=b"""\ # -*- coding: iso8859-1 -*- # A Python source file in another encoding. @@ -817,7 +832,7 @@ assert len(math) == 18 '<span class="str">"3×4 = 12, ÷2 = 6±0"</span>', ) - def make_main_etc(self): + def make_main_etc(self) -> None: """Make main.py and m1-m3.py for other tests.""" self.make_file("main.py", """\ import m1 @@ -844,28 +859,28 @@ assert len(math) == 18 m3b = 2 """) - def test_omit_1(self): + def test_omit_1(self) -> None: self.make_main_etc() cov = coverage.Coverage(include=["./*"]) self.start_import_stop(cov, "main") cov.html_report(directory="out/omit_1") compare_html(gold_path("html/omit_1"), "out/omit_1") - def test_omit_2(self): + def test_omit_2(self) -> None: self.make_main_etc() cov = coverage.Coverage(include=["./*"]) self.start_import_stop(cov, "main") cov.html_report(directory="out/omit_2", omit=["m1.py"]) compare_html(gold_path("html/omit_2"), "out/omit_2") - def test_omit_3(self): + def test_omit_3(self) -> None: self.make_main_etc() cov = coverage.Coverage(include=["./*"]) self.start_import_stop(cov, "main") cov.html_report(directory="out/omit_3", omit=["m1.py", "m2.py"]) compare_html(gold_path("html/omit_3"), "out/omit_3") - def test_omit_4(self): + def test_omit_4(self) -> None: self.make_main_etc() self.make_file("omit4.ini", """\ [report] @@ -877,7 +892,7 @@ assert len(math) == 18 cov.html_report(directory="out/omit_4") compare_html(gold_path("html/omit_4"), "out/omit_4") - def test_omit_5(self): + def test_omit_5(self) -> None: self.make_main_etc() self.make_file("omit5.ini", """\ [report] @@ -895,7 +910,7 @@ assert len(math) == 18 cov.html_report() compare_html(gold_path("html/omit_5"), "out/omit_5") - def test_other(self): + def test_other(self) -> None: self.make_file("src/here.py", """\ import other @@ -935,7 +950,7 @@ assert len(math) == 18 'other.py</a>', ) - def test_partial(self): + def test_partial(self) -> None: self.make_file("partial.py", """\ # partial branches and excluded lines a = 2 @@ -1002,7 +1017,7 @@ assert len(math) == 18 '<span class="pc_cov">91%</span>', ) - def test_styled(self): + def test_styled(self) -> None: self.make_file("a.py", """\ if 1 < 2: # Needed a < to look at HTML entities. @@ -1035,7 +1050,7 @@ assert len(math) == 18 '<span class="pc_cov">67%</span>', ) - def test_tabbed(self): + def test_tabbed(self) -> None: # The file contents would look like this with 8-space tabs: # x = 1 # if x: @@ -1069,7 +1084,7 @@ assert len(math) == 18 doesnt_contain("out/tabbed_py.html", "\t") - def test_unicode(self): + def test_unicode(self) -> None: surrogate = "\U000e0100" self.make_file("unicode.py", """\ @@ -1096,7 +1111,7 @@ assert len(math) == 18 '<span class="str">"db40,dd00: x󠄀"</span>', ) - def test_accented_dot_py(self): + def test_accented_dot_py(self) -> None: # Make a file with a non-ascii character in the filename. self.make_file("h\xe2t.py", "print('accented')") self.make_data_file(lines={abs_file("h\xe2t.py"): [1]}) @@ -1108,7 +1123,7 @@ assert len(math) == 18 index = indexf.read() assert '<a href="hât_py.html">hât.py</a>' in index - def test_accented_directory(self): + def test_accented_directory(self) -> None: # Make a file with a non-ascii character in the directory name. self.make_file("\xe2/accented.py", "print('accented')") self.make_data_file(lines={abs_file("\xe2/accented.py"): [1]}) @@ -1129,7 +1144,7 @@ class HtmlWithContextsTest(HtmlTestHelpers, CoverageTest): EMPTY = coverage.html.HtmlDataGeneration.EMPTY - def html_data_from_cov(self, cov, morf): + def html_data_from_cov(self, cov: Coverage, morf: TMorf) -> coverage.html.FileData: """Get HTML report data from a `Coverage` object for a morf.""" with self.assert_warnings(cov, []): datagen = coverage.html.HtmlDataGeneration(cov) @@ -1166,7 +1181,7 @@ class HtmlWithContextsTest(HtmlTestHelpers, CoverageTest): TEST_ONE_LINES = [5, 6, 2] TEST_TWO_LINES = [9, 10, 11, 13, 14, 15, 2] - def test_dynamic_contexts(self): + def test_dynamic_contexts(self) -> None: self.make_file("two_tests.py", self.SOURCE) cov = coverage.Coverage(source=["."]) cov.set_option("run:dynamic_context", "test_function") @@ -1182,7 +1197,7 @@ class HtmlWithContextsTest(HtmlTestHelpers, CoverageTest): ] assert sorted(expected) == sorted(actual) - def test_filtered_dynamic_contexts(self): + def test_filtered_dynamic_contexts(self) -> None: self.make_file("two_tests.py", self.SOURCE) cov = coverage.Coverage(source=["."]) cov.set_option("run:dynamic_context", "test_function") @@ -1192,12 +1207,12 @@ class HtmlWithContextsTest(HtmlTestHelpers, CoverageTest): d = self.html_data_from_cov(cov, mod) context_labels = [self.EMPTY, 'two_tests.test_one', 'two_tests.test_two'] - expected_lines = [[], self.TEST_ONE_LINES, []] + expected_lines: List[List[TLineNo]] = [[], self.TEST_ONE_LINES, []] for label, expected in zip(context_labels, expected_lines): actual = [ld.number for ld in d.lines if label in (ld.contexts or ())] assert sorted(expected) == sorted(actual) - def test_no_contexts_warns_no_contexts(self): + def test_no_contexts_warns_no_contexts(self) -> None: # If no contexts were collected, then show_contexts emits a warning. self.make_file("two_tests.py", self.SOURCE) cov = coverage.Coverage(source=["."]) @@ -1206,7 +1221,7 @@ class HtmlWithContextsTest(HtmlTestHelpers, CoverageTest): with self.assert_warnings(cov, ["No contexts were measured"]): cov.html_report() - def test_dynamic_contexts_relative_files(self): + def test_dynamic_contexts_relative_files(self) -> None: self.make_file("two_tests.py", self.SOURCE) self.make_file("config", "[run]\nrelative_files = True") cov = coverage.Coverage(source=["."], config_file="config") @@ -1227,14 +1242,14 @@ class HtmlWithContextsTest(HtmlTestHelpers, CoverageTest): class HtmlHelpersTest(HtmlTestHelpers, CoverageTest): """Tests of the helpers in HtmlTestHelpers.""" - def test_bad_link(self): + def test_bad_link(self) -> None: # Does assert_valid_hrefs detect links to non-existent files? self.make_file("htmlcov/index.html", "<a href='nothing.html'>Nothing</a>") msg = "These files link to 'nothing.html', which doesn't exist: htmlcov.index.html" with pytest.raises(AssertionError, match=msg): self.assert_valid_hrefs() - def test_bad_anchor(self): + def test_bad_anchor(self) -> None: # Does assert_valid_hrefs detect fragments that go nowhere? self.make_file("htmlcov/index.html", "<a href='#nothing'>Nothing</a>") msg = "Fragment '#nothing' in htmlcov.index.html has no anchor" @@ -98,7 +98,7 @@ setenv = C_AN=coverage/config.py coverage/data.py coverage/disposition.py coverage/files.py coverage/inorout.py coverage/multiproc.py coverage/numbits.py C_OP=coverage/parser.py coverage/phystokens.py coverage/plugin.py coverage/python.py C_QZ=coverage/results.py coverage/sqldata.py coverage/tomlconfig.py coverage/types.py - T_AN=tests/test_api.py tests/helpers.py + T_AN=tests/test_api.py tests/goldtest.py tests/helpers.py tests/test_html.py TYPEABLE={env:C_AN} {env:C_OP} {env:C_QZ} {env:T_AN} commands = |