Skip to content

Commit 9be8589

Browse files
committed
Add type-checking with mypy
Also fix the mishandling of the return type described in #84. Fixes #84, #85.
1 parent df38531 commit 9be8589

File tree

10 files changed

+277
-86
lines changed

10 files changed

+277
-86
lines changed

.pre-commit-config.yaml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
exclude: '^($|.*\.bin)'
1+
default_language_version:
2+
python: "3.10"
23
repos:
34
- repo: https://github.com/asottile/reorder_python_imports
45
rev: v3.8.2
@@ -11,7 +12,6 @@ repos:
1112
hooks:
1213
- id: black
1314
args: [--safe, --quiet]
14-
language_version: python3.7
1515
- repo: https://github.com/pre-commit/pre-commit-hooks
1616
rev: v4.2.0
1717
hooks:
@@ -25,3 +25,11 @@ repos:
2525
files: README.rst
2626
language: python
2727
additional_dependencies: [pygments, restructuredtext_lint]
28+
- repo: https://github.com/pre-commit/mirrors-mypy
29+
rev: v0.971
30+
hooks:
31+
- id: mypy
32+
files: src/
33+
args: []
34+
additional_dependencies:
35+
- pytest>=7

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# 2.2.1 (UNRELEASED)
2+
3+
- Fixed a problem while handling errors using``--gtest_filter``. The recommendation is to use pytest's own
4+
filtering facilities (like `-k`) instead of passing filtering arguments to the underlying framework
5+
([#84](https://github.com/pytest-dev/pytest-cpp/issues/84)).
6+
7+
18
# 2.2.0 (2022-08-22)
29

310
- Dropped support for Python 3.6.

README.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ pytest's ``-o`` option:
9999
100100
$ pytest -o cpp_arguments='-v --log-dir=logs'
101101
102+
**Important**: do not pass filtering arguments (for example ``--gtest_filter``), as this will conflict
103+
with the plugin functionality and behave incorrectly.
104+
105+
To filter tests, use the standard pytest filtering facilities (such as ``-k``).
106+
102107
cpp_ignore_py_files
103108
^^^^^^^^^^^^^^^^^^^
104109

mypy.ini

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[mypy]
2+
files = src
3+
check_untyped_defs = True
4+
disallow_any_generics = True
5+
ignore_missing_imports = True
6+
no_implicit_optional = True
7+
show_error_codes = True
8+
strict_equality = True
9+
warn_redundant_casts = True
10+
warn_return_any = True
11+
warn_unreachable = True
12+
warn_unused_configs = True
13+
no_implicit_reexport = True
14+
local_partial_types = True
15+
disallow_untyped_defs = True
16+
; workaround for https://github.com/python/mypy/issues/10709
17+
ignore_missing_imports_per_module = True

src/pytest_cpp/boost.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
1+
from __future__ import annotations
2+
13
import io
24
import os
35
import shutil
46
import subprocess
57
import tempfile
8+
from typing import Sequence
69
from xml.etree import ElementTree
710

811
from pytest_cpp.error import CppTestFailure
12+
from pytest_cpp.error import Markup
13+
from pytest_cpp.facade_abc import AbstractFacade
914

1015

11-
class BoostTestFacade(object):
16+
class BoostTestFacade(AbstractFacade):
1217
"""
1318
Facade for BoostTests.
1419
"""
1520

1621
@classmethod
17-
def is_test_suite(cls, executable):
22+
def is_test_suite(cls, executable: str) -> bool:
1823
try:
1924
output = subprocess.check_output(
2025
[executable, "--help"],
@@ -26,25 +31,29 @@ def is_test_suite(cls, executable):
2631
else:
2732
return "--output_format" in output and "log_format" in output
2833

29-
def list_tests(self, executable):
34+
def list_tests(self, executable: str) -> list[str]:
3035
# unfortunately boost doesn't provide us with a way to list the tests
3136
# inside the executable, so the test_id is a dummy placeholder :(
3237
return [os.path.basename(os.path.splitext(executable)[0])]
3338

34-
def run_test(self, executable, test_id, test_args=(), harness=None):
35-
harness = harness or []
36-
37-
def read_file(name):
39+
def run_test(
40+
self,
41+
executable: str,
42+
test_id: str,
43+
test_args: Sequence[str] = (),
44+
harness: Sequence[str] = (),
45+
) -> tuple[Sequence[BoostTestFailure] | None, str]:
46+
def read_file(name: str) -> str:
3847
try:
3948
with io.open(name) as f:
4049
return f.read()
4150
except IOError:
42-
return None
51+
return ""
4352

4453
temp_dir = tempfile.mkdtemp()
4554
log_xml = os.path.join(temp_dir, "log.xml")
4655
report_xml = os.path.join(temp_dir, "report.xml")
47-
args = harness + [
56+
args = list(harness) + [
4857
executable,
4958
"--output_format=XML",
5059
"--log_sink=%s" % log_xml,
@@ -88,7 +97,7 @@ def read_file(name):
8897

8998
return None, stdout
9099

91-
def _parse_log(self, log):
100+
def _parse_log(self, log: str) -> list[BoostTestFailure]:
92101
"""
93102
Parse the "log" section produced by BoostTest.
94103
@@ -116,19 +125,19 @@ def _parse_log(self, log):
116125
for elem in parsed_elements:
117126
filename = elem.attrib["file"]
118127
linenum = int(elem.attrib["line"])
119-
result.append(BoostTestFailure(filename, linenum, elem.text))
128+
result.append(BoostTestFailure(filename, linenum, elem.text or ""))
120129
return result
121130

122131

123132
class BoostTestFailure(CppTestFailure):
124-
def __init__(self, filename, linenum, contents):
133+
def __init__(self, filename: str, linenum: int, contents: str) -> None:
125134
self.filename = filename
126135
self.linenum = linenum
127136
self.lines = contents.splitlines()
128137

129-
def get_lines(self):
138+
def get_lines(self) -> list[tuple[str, Markup]]:
130139
m = ("red", "bold")
131140
return [(x, m) for x in self.lines]
132141

133-
def get_file_reference(self):
142+
def get_file_reference(self) -> tuple[str, int]:
134143
return self.filename, self.linenum

src/pytest_cpp/catch2.py

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
1+
from __future__ import annotations
2+
13
import os
24
import subprocess
35
import tempfile
6+
from typing import Sequence
47
from xml.etree import ElementTree
58

69
import pytest
710

811
from pytest_cpp.error import CppTestFailure
12+
from pytest_cpp.error import Markup
13+
from pytest_cpp.facade_abc import AbstractFacade
914

1015

11-
class Catch2Facade(object):
16+
class Catch2Facade(AbstractFacade):
1217
"""
1318
Facade for Catch2.
1419
"""
1520

1621
@classmethod
17-
def is_test_suite(cls, executable):
22+
def is_test_suite(cls, executable: str) -> bool:
1823
try:
1924
output = subprocess.check_output(
2025
[executable, "--help"],
@@ -26,7 +31,7 @@ def is_test_suite(cls, executable):
2631
else:
2732
return "--list-test-names-only" in output
2833

29-
def list_tests(self, executable):
34+
def list_tests(self, executable: str) -> list[str]:
3035
"""
3136
Executes test with "--list-test-names-only" and gets list of tests
3237
parsing output like this:
@@ -49,10 +54,15 @@ def list_tests(self, executable):
4954

5055
return result
5156

52-
def run_test(self, executable, test_id="", test_args=(), harness=None):
53-
harness = harness or []
57+
def run_test(
58+
self,
59+
executable: str,
60+
test_id: str = "",
61+
test_args: Sequence[str] = (),
62+
harness: Sequence[str] = (),
63+
) -> tuple[Sequence[Catch2Failure] | None, str]:
5464
xml_filename = self._get_temp_xml_filename()
55-
args = harness + [
65+
args = list(harness) + [
5666
executable,
5767
test_id,
5868
"--success",
@@ -103,16 +113,20 @@ def run_test(self, executable, test_id="", test_args=(), harness=None):
103113
else:
104114
return None, output
105115

106-
msg = "Internal Error: could not find test " "{test_id} in results:\n{results}"
116+
msg = "Internal Error: could not find test {test_id} in results:\n{results}"
107117

108-
results_list = "\n".join("\n".join(x) for (n, x, f) in results)
109-
failure = Catch2Failure(msg.format(test_id=test_id, results=results_list))
118+
results_list = "\n".join(n for (n, x, f) in results)
119+
failure = Catch2Failure(
120+
msg.format(test_id=test_id, results=results_list), 0, ""
121+
)
110122
return [failure], output
111123

112-
def _get_temp_xml_filename(self):
124+
def _get_temp_xml_filename(self) -> str:
113125
return tempfile.mktemp()
114126

115-
def _parse_xml(self, xml_filename):
127+
def _parse_xml(
128+
self, xml_filename: str
129+
) -> Sequence[tuple[str, Sequence[tuple[str, int, str]], bool]]:
116130
root = ElementTree.parse(xml_filename)
117131
result = []
118132
for test_suite in root.findall("Group"):
@@ -121,14 +135,16 @@ def _parse_xml(self, xml_filename):
121135
test_name = test_case.attrib["name"]
122136
test_result = test_case.find("OverallResult")
123137
failures = []
124-
if test_result.attrib["success"] == "false":
138+
if test_result is not None and test_result.attrib["success"] == "false":
125139
test_checks = test_case.findall("Expression")
126140
for check in test_checks:
127141
file_name = check.attrib["filename"]
128-
line_num = check.attrib["line"]
129-
if check.attrib["success"] == "false":
130-
expected = check.find("Original").text
131-
actual = check.find("Expanded").text
142+
line_num = int(check.attrib["line"])
143+
if check is not None and check.attrib["success"] == "false":
144+
item = check.find("Original")
145+
expected = item.text if item is not None else ""
146+
item = check.find("Expanded")
147+
actual = item.text if item is not None else ""
132148
fail_msg = "Expected: {expected}\nActual: {actual}".format(
133149
expected=expected, actual=actual
134150
)
@@ -146,14 +162,14 @@ def _parse_xml(self, xml_filename):
146162

147163

148164
class Catch2Failure(CppTestFailure):
149-
def __init__(self, filename, linenum, lines):
165+
def __init__(self, filename: str, linenum: int, lines: str):
150166
self.lines = lines.splitlines()
151167
self.filename = filename
152-
self.linenum = int(linenum)
168+
self.linenum = linenum
153169

154-
def get_lines(self):
170+
def get_lines(self) -> list[tuple[str, Markup]]:
155171
m = ("red", "bold")
156172
return [(x, m) for x in self.lines]
157173

158-
def get_file_reference(self):
174+
def get_file_reference(self) -> tuple[str, int]:
159175
return self.filename, self.linenum

src/pytest_cpp/error.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1+
from __future__ import annotations
2+
13
import os
24
import string
5+
from typing import Sequence
6+
from typing import Tuple
37

48
from _pytest._code.code import ReprFileLocation
9+
from _pytest._io import TerminalWriter
510

611

712
class CppFailureError(Exception):
813
"""
914
Should be raised by test Facades when a test fails.
10-
11-
Contains a list of `CppFailure` instances.
1215
"""
1316

14-
def __init__(self, failures):
15-
self.failures = failures
17+
def __init__(self, failures: Sequence[CppTestFailure]) -> None:
18+
self.failures = list(failures)
19+
20+
21+
Markup = Tuple[str, ...]
1622

1723

1824
class CppTestFailure(object):
@@ -22,7 +28,7 @@ class CppTestFailure(object):
2228
message that will be displayed in the terminal.
2329
"""
2430

25-
def get_lines(self):
31+
def get_lines(self) -> list[tuple[str, Markup]]:
2632
"""
2733
Returns list of (line, markup) that will be displayed to the user,
2834
where markup can be a sequence of color codes from
@@ -34,7 +40,7 @@ def get_lines(self):
3440
"""
3541
raise NotImplementedError # pragma: no cover
3642

37-
def get_file_reference(self):
43+
def get_file_reference(self) -> tuple[str, int]:
3844
"""
3945
Return tuple of filename, linenum of the failure.
4046
"""
@@ -49,22 +55,22 @@ class CppFailureRepr(object):
4955

5056
failure_sep = "---"
5157

52-
def __init__(self, failures):
53-
self.failures = failures
58+
def __init__(self, failures: Sequence[CppTestFailure]) -> None:
59+
self.failures = list(failures)
5460

55-
def __str__(self):
61+
def __str__(self) -> str:
5662
reprs = []
5763
for failure in self.failures:
5864
pure_lines = "\n".join(x[0] for x in failure.get_lines())
5965
repr_loc = self._get_repr_file_location(failure)
6066
reprs.append("%s\n%s" % (pure_lines, repr_loc))
6167
return self.failure_sep.join(reprs)
6268

63-
def _get_repr_file_location(self, failure):
69+
def _get_repr_file_location(self, failure: CppTestFailure) -> ReprFileLocation:
6470
filename, linenum = failure.get_file_reference()
6571
return ReprFileLocation(filename, linenum, "C++ failure")
6672

67-
def toterminal(self, tw):
73+
def toterminal(self, tw: TerminalWriter) -> None:
6874
for index, failure in enumerate(self.failures):
6975
filename, linenum = failure.get_file_reference()
7076
code_lines = get_code_context_around_line(filename, linenum)
@@ -84,7 +90,7 @@ def toterminal(self, tw):
8490
tw.line(self.failure_sep, cyan=True)
8591

8692

87-
def get_code_context_around_line(filename, linenum):
93+
def get_code_context_around_line(filename: str, linenum: int) -> list[str]:
8894
"""
8995
return code context lines, with the last line being the line at
9096
linenum.
@@ -98,7 +104,7 @@ def get_code_context_around_line(filename, linenum):
98104
return []
99105

100106

101-
def get_left_whitespace(line):
107+
def get_left_whitespace(line: str) -> str:
102108
result = ""
103109
for c in line:
104110
if c in string.whitespace:

0 commit comments

Comments
 (0)