Skip to content

Commit 180c6d8

Browse files
committed
chore: strict type annotations of test directories
1 parent 7ce77ae commit 180c6d8

13 files changed

+179
-113
lines changed

tests/extensions/test_base_extensions.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@
1010

1111

1212
class FakeExtension(SimpleRegexAnnotationExtension):
13-
extension_name = 'fake_extension'
13+
extension_name: str = 'fake_extension'
1414

15-
lang_comment_definition = {
15+
lang_comment_definition: dict[str, str] = {
1616
'multi_start': re.escape('foo'),
1717
'multi_end': re.escape('bar'),
1818
'single': re.escape('baz')
1919
}
2020

2121

22-
def test_nothing_found():
22+
def test_nothing_found() -> None:
2323
"""
2424
Make sure nothing fails when no annotation is found.
2525
"""
@@ -30,7 +30,7 @@ def test_nothing_found():
3030
r.search(f)
3131

3232

33-
def test_strip_single_line_comment_tokens():
33+
def test_strip_single_line_comment_tokens() -> None:
3434
config = FakeConfig()
3535

3636
extension = FakeExtension(config, VerboseEcho())

tests/extensions/test_extension_javascript.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Tests for the Javascript static extension
33
"""
4+
45
import pytest
56

67
from tests.helpers import EXIT_CODE_FAILURE, EXIT_CODE_SUCCESS, call_script
@@ -20,7 +21,7 @@
2021
('choice_failures_4.js', EXIT_CODE_FAILURE, '"terrible" is already present in this annotation'),
2122
('choice_failures_5.js', EXIT_CODE_FAILURE, 'no value found for ".. ignored:"'),
2223
])
23-
def test_grouping_and_choice_failures(test_file, expected_exit_code, expected_message):
24+
def test_grouping_and_choice_failures(test_file: str, expected_exit_code: int, expected_message: str) -> None:
2425
result = call_script((
2526
'static_find_annotations',
2627
'--config_file',

tests/extensions/test_extension_python.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
('choice_failures_4.pyt', EXIT_CODE_FAILURE, '"terrible" is already present in this annotation'),
2222
('choice_failures_5.pyt', EXIT_CODE_FAILURE, 'no value found for ".. ignored:"'),
2323
])
24-
def test_grouping_and_choice_failures(test_file, expected_exit_code, expected_message):
24+
def test_grouping_and_choice_failures(test_file: str, expected_exit_code: int, expected_message: str) -> None:
2525
result = call_script((
2626
'static_find_annotations',
2727
'--config_file',
@@ -86,7 +86,7 @@ def test_grouping_and_choice_failures(test_file, expected_exit_code, expected_me
8686
]
8787
),
8888
])
89-
def test_multi_line_annotations(test_file, annotations):
89+
def test_multi_line_annotations(test_file: str, annotations: list[tuple[str, str]]) -> None:
9090
config = AnnotationConfig('tests/test_configurations/.annotations_test')
9191
annotator = PythonAnnotationExtension(config, VerboseEcho())
9292

tests/helpers.py

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@
33
"""
44
import os
55
import re
6+
import typing as t
7+
from collections.abc import Callable, Sequence
68

79
from click.testing import CliRunner
10+
from click.testing import Result as ClickTestResult
11+
from stevedore import named
812

9-
from code_annotations.base import BaseSearch, VerboseEcho
13+
from code_annotations.base import AnnotationConfig, BaseSearch
1014
from code_annotations.cli import entry_point
15+
from code_annotations.helpers import VerboseEcho
16+
17+
# Re-export Result for convenience
18+
Result = ClickTestResult
1119

1220
EXIT_CODE_SUCCESS = 0
1321
EXIT_CODE_FAILURE = 1
@@ -34,30 +42,48 @@
3442
""".format(DEFAULT_FAKE_SAFELIST_PATH)
3543

3644

37-
class FakeConfig:
45+
class FakeConfig(AnnotationConfig):
3846
"""
3947
Simple config for testing without reading a config file.
4048
"""
4149

42-
annotations: dict[str, str] = {}
43-
annotation_regexes: list[str] = []
44-
annotation_tokens: list[str] = []
45-
groups: list[str] = []
46-
echo = VerboseEcho()
50+
# annotations: dict[str, t.Any] = {}
51+
# annotation_regexes: list[str] = []
52+
# annotation_tokens: list[str] = []
53+
# groups: t.Union[list[str], dict[str, list[str]]] = []
54+
# echo = VerboseEcho()
55+
56+
def __init__(self) -> None: # pylint: disable=super-init-not-called
57+
"""
58+
Override the base __init__ to skip reading and parsing the config.
59+
"""
60+
self.groups: dict[str, list[str]] = {}
61+
self.choices: dict[str, list[str]] = {}
62+
self.optional_groups: list[str] = []
63+
self.annotation_tokens: list[str] = []
64+
self.annotation_regexes: list[str] = []
65+
self.mgr: named.NamedExtensionManager | None = None
66+
self.coverage_target: float | None = None
67+
self.echo = VerboseEcho()
68+
4769

4870

4971
class FakeSearch(BaseSearch):
5072
"""
5173
Simple test class for directly testing BaseSearch since it's abstract.
5274
"""
5375

54-
def search(self):
76+
def search(self) -> dict[str, list[dict[str, t.Any]]]:
5577
"""
5678
Override for abstract base method.
79+
80+
Returns:
81+
Empty dict to satisfy the abstract method requirement
5782
"""
83+
return {}
5884

5985

60-
def delete_report_files(file_extension):
86+
def delete_report_files(file_extension: str) -> None:
6187
"""
6288
Delete all files with the given extension from the test_reports directory.
6389
@@ -76,7 +102,7 @@ def delete_report_files(file_extension):
76102
pass
77103

78104

79-
def call_script(args_list, delete_test_reports=True, delete_test_docs=True):
105+
def call_script(args_list: Sequence[str], delete_test_reports: bool = True, delete_test_docs: bool = True) -> Result:
80106
"""
81107
Call the code_annotations script with the given params and a generic config file.
82108
@@ -108,11 +134,11 @@ def call_script(args_list, delete_test_reports=True, delete_test_docs=True):
108134

109135

110136
def call_script_isolated(
111-
args_list,
112-
test_filesystem_cb=None,
113-
test_filesystem_report_cb=None,
114-
fake_safelist_data="{}"
115-
):
137+
args_list: list[str],
138+
test_filesystem_cb: Callable[[], None] | None = None,
139+
test_filesystem_report_cb: Callable[[str], None] | None = None,
140+
fake_safelist_data: str = "{}"
141+
) -> Result:
116142
"""
117143
Call the code_annotations script with the given params and a generic config file.
118144
@@ -123,8 +149,6 @@ def call_script_isolated(
123149
test_filesystem_report_cb: Callback function, called after the command is run, before the temp filesystem
124150
is cleared. Callback is called with the raw text contents of the report file.
125151
fake_safelist_data: Raw text to write to the safelist file before the command is called.
126-
safelist_path: File path to write the safelist to. Used when writing a fake safelist, but not automatically
127-
passed to the command.
128152
129153
Returns:
130154
click.testing.Result: Result from the `CliRunner.invoke()` call.
@@ -151,7 +175,9 @@ def call_script_isolated(
151175

152176
if test_filesystem_report_cb:
153177
try:
154-
report_file = re.search(r'Generating report to (.*)', result.output).groups()[0]
178+
report_match = re.search(r'Generating report to (.*)', result.output)
179+
assert report_match is not None
180+
report_file = report_match.groups()[0]
155181
with open(report_file) as f:
156182
report_contents = f.read()
157183

@@ -163,7 +189,7 @@ def call_script_isolated(
163189
return result
164190

165191

166-
def get_report_filename_from_output(output):
192+
def get_report_filename_from_output(output: str) -> str | None:
167193
"""
168194
Find the report filename in a find_static or find_django output and return it.
169195
@@ -172,9 +198,10 @@ def get_report_filename_from_output(output):
172198
173199
Returns:
174200
Filename of the found report, or None of no name is found
175-
176201
"""
202+
match = re.search(r'Generating report to (.*)', output)
203+
assert match is not None
177204
try:
178-
return re.search(r'Generating report to (.*)', output).groups()[0]
205+
return match.groups()[0]
179206
except IndexError:
180207
return None

tests/test_base.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
"""
22
Tests for code_annotations/base.py
33
"""
4+
import typing as t
45
from collections import OrderedDict
56

67
import pytest
78

8-
from code_annotations.base import AnnotationConfig, ConfigurationException
9+
from code_annotations.base import AnnotationConfig
10+
from code_annotations.exceptions import ConfigurationException
911
from tests.helpers import FakeConfig, FakeSearch
1012

1113

12-
def test_get_group_for_token_missing_token():
14+
def test_get_group_for_token_missing_token() -> None:
1315
config = FakeConfig()
1416
search = FakeSearch(config)
1517
assert search._get_group_for_token('foo') is None # pylint: disable=protected-access
1618

1719

18-
def test_get_group_for_token_multiple_groups():
20+
def test_get_group_for_token_multiple_groups() -> None:
1921
config = FakeConfig()
2022
config.groups = {
2123
'group1': ['token1'],
@@ -30,7 +32,7 @@ def test_get_group_for_token_multiple_groups():
3032
('.annotations_test_missing_report_path', "report_path"),
3133
('.annotations_test_missing_safelist_path', "safelist_path"),
3234
])
33-
def test_missing_config(test_config, expected_message):
35+
def test_missing_config(test_config: str, expected_message: str) -> None:
3436
with pytest.raises(ConfigurationException) as exception:
3537
AnnotationConfig(f'tests/test_configurations/{test_config}', None, 3)
3638

@@ -44,15 +46,15 @@ def test_missing_config(test_config, expected_message):
4446
('.annotations_test_coverage_over_100', "Invalid coverage target. 150.0 is not between 0 and 100."),
4547
('.annotations_test_coverage_nan', 'Coverage target must be a number between 0 and 100 not "not a number".'),
4648
])
47-
def test_bad_coverage_targets(test_config, expected_message):
49+
def test_bad_coverage_targets(test_config: str, expected_message: str) -> None:
4850
with pytest.raises(ConfigurationException) as exception:
4951
AnnotationConfig(f'tests/test_configurations/{test_config}', None, 3)
5052

5153
exc_msg = str(exception.value)
5254
assert expected_message in exc_msg
5355

5456

55-
def test_coverage_target_int():
57+
def test_coverage_target_int() -> None:
5658
# We just care that this doesn't throw an exception
5759
AnnotationConfig('tests/test_configurations/{}'.format('.annotations_test_coverage_int'), None, 3)
5860

@@ -64,15 +66,15 @@ def test_coverage_target_int():
6466
('.annotations_test_group_one_token', 'Group "pii_group" must have more than one annotation.'),
6567
('.annotations_test_group_bad_type', "{'.. pii:': ['bad', 'type']} is an unknown annotation type."),
6668
])
67-
def test_annotation_configuration_errors(test_config, expected_message):
69+
def test_annotation_configuration_errors(test_config: str, expected_message: str) -> None:
6870
with pytest.raises(ConfigurationException) as exception:
6971
AnnotationConfig(f'tests/test_configurations/{test_config}', None, 3)
7072

7173
exc_msg = str(exception.value)
7274
assert expected_message in exc_msg
7375

7476

75-
def test_format_results_for_report():
77+
def test_format_results_for_report() -> None:
7678
"""
7779
Test that report formatting puts annotations into groups correctly
7880
"""
@@ -86,7 +88,7 @@ def test_format_results_for_report():
8688
search = FakeSearch(config)
8789

8890
# Create a fake result set for _format_results_for_report to work on
89-
fake_results = OrderedDict()
91+
fake_results: OrderedDict[str, list[dict[str, t.Any]]] = OrderedDict()
9092

9193
# First file has 6 annotations. expected_group_id is a special key for this test, allowing us to loop through
9294
# these below and know what group each result should be in.

tests/test_django_coverage.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""
22
Tests for the DjangoSearch coverage functionality.
33
"""
4-
from unittest.mock import DEFAULT, patch
4+
import typing as t
5+
from unittest.mock import DEFAULT, MagicMock, patch
56

67
import pytest
78

@@ -21,7 +22,10 @@
2122
)
2223
from tests.helpers import EXIT_CODE_FAILURE, EXIT_CODE_SUCCESS, call_script_isolated
2324

24-
ALL_FAKE_MODELS = (
25+
# Type for our fake model classes
26+
FakeModelClass = t.Type[t.Any]
27+
28+
ALL_FAKE_MODELS: tuple[FakeModelClass, ...] = (
2529
FakeBaseModelAbstract,
2630
FakeBaseModelBoring,
2731
FakeBaseModelBoringWithAnnotations,
@@ -40,10 +44,15 @@
4044
@patch('code_annotations.find_django.DjangoSearch.setup_django')
4145
@patch('code_annotations.find_django.DjangoSearch.is_non_local')
4246
@patch('code_annotations.find_django.django.apps.apps.get_app_configs')
43-
def test_coverage_all_models(mock_get_app_configs, mock_is_non_local, mock_setup_django, mock_issubclass):
47+
def test_coverage_all_models(
48+
mock_get_app_configs: MagicMock,
49+
mock_is_non_local: MagicMock,
50+
mock_setup_django: MagicMock,
51+
mock_issubclass: MagicMock
52+
) -> None:
4453
# Lots of fakery going on here. This class mocks Django AppConfigs to deliver our fake models.
4554
class FakeAppConfig:
46-
def get_models(self):
55+
def get_models(self) -> tuple[FakeModelClass, ...]:
4756
return ALL_FAKE_MODELS
4857

4958
# This lets us deterministically decide that one model is local, and the other isn't, for testing both branches.
@@ -107,8 +116,13 @@ def get_models(self):
107116
"Coverage is 100.0%"
108117
),
109118
])
110-
def test_coverage_thresholds(local_models, should_succeed, expected_message, **kwargs):
111-
mock_get_models_requiring_annotations = kwargs['get_models_requiring_annotations']
119+
def test_coverage_thresholds(
120+
local_models: list[FakeModelClass],
121+
should_succeed: bool,
122+
expected_message: str,
123+
**kwargs: t.Any
124+
) -> None:
125+
mock_get_models_requiring_annotations: MagicMock = kwargs['get_models_requiring_annotations']
112126
mock_get_models_requiring_annotations.return_value = (
113127
set(local_models),
114128
set(),

0 commit comments

Comments
 (0)