summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2022-12-31 14:53:21 -0500
committerNed Batchelder <ned@nedbatchelder.com>2022-12-31 14:53:21 -0500
commitbf73b37080c3c6deec969a555b45b70ee6727b13 (patch)
tree98f6c9c20d89afcc4d0c2396a155229fbd802290
parentee1e4150529e55cd860fc3628b820d3a2ed471de (diff)
downloadpython-coveragepy-git-bf73b37080c3c6deec969a555b45b70ee6727b13.tar.gz
mypy: check tests/goldtest.py, tests/test_html.py
-rw-r--r--coverage/config.py6
-rw-r--r--coverage/control.py12
-rw-r--r--coverage/html.py75
-rw-r--r--coverage/types.py2
-rw-r--r--tests/goldtest.py44
-rw-r--r--tests/test_html.py163
-rw-r--r--tox.ini2
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 &amp; stuff!</title>" in index
assert "<h1>Metrics &amp; 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>&#171;&#964;&#945;&#1041;&#1068;&#8467;&#963;&#187; numbers" in index
assert "<h1>&#171;&#964;&#945;&#1041;&#1068;&#8467;&#963;&#187; 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?&#65533;!"
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&#215;4 = 12, &#247;2 = 6&#177;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&#215;4 = 12, &#247;2 = 6&#177;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&#917760;"</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&#226;t_py.html">h&#226;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"
diff --git a/tox.ini b/tox.ini
index 76c70daa..6e08adc1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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 =