Skip to content

Commit 1d87145

Browse files
committed
chore: strict type annotations of test directories
1 parent 755806f commit 1d87145

13 files changed

+166
-114
lines changed

tests/extensions/test_base_extensions.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,30 @@
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
"""
2626
config = FakeConfig()
2727

28-
r = FakeExtension(config, VerboseEcho())
28+
r = FakeExtension(config, VerboseEcho()) # type: ignore
2929
with open('tests/extensions/base_test_files/empty.foo') as f:
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

36-
extension = FakeExtension(config, VerboseEcho())
36+
extension = FakeExtension(config, VerboseEcho()) # type: ignore
3737
text = """baz line1
3838
baz line2
3939
bazline3

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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""
22
Tests for the Python static extension
33
"""
4+
from typing import List, Tuple
5+
46
import pytest
57

68
from code_annotations.base import AnnotationConfig
@@ -21,7 +23,7 @@
2123
('choice_failures_4.pyt', EXIT_CODE_FAILURE, '"terrible" is already present in this annotation'),
2224
('choice_failures_5.pyt', EXIT_CODE_FAILURE, 'no value found for ".. ignored:"'),
2325
])
24-
def test_grouping_and_choice_failures(test_file, expected_exit_code, expected_message):
26+
def test_grouping_and_choice_failures(test_file: str, expected_exit_code: int, expected_message: str) -> None:
2527
result = call_script((
2628
'static_find_annotations',
2729
'--config_file',
@@ -86,7 +88,7 @@ def test_grouping_and_choice_failures(test_file, expected_exit_code, expected_me
8688
]
8789
),
8890
])
89-
def test_multi_line_annotations(test_file, annotations):
91+
def test_multi_line_annotations(test_file: str, annotations: List[Tuple[str, str]]) -> None:
9092
config = AnnotationConfig('tests/test_configurations/.annotations_test')
9193
annotator = PythonAnnotationExtension(config, VerboseEcho())
9294

tests/helpers.py

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@
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
811

9-
from code_annotations.base import BaseSearch, VerboseEcho
12+
from code_annotations.base import BaseSearch
1013
from code_annotations.cli import entry_point
14+
from code_annotations.helpers import VerboseEcho
15+
16+
# Re-export Result for convenience
17+
Result = ClickTestResult
1118

1219
EXIT_CODE_SUCCESS = 0
1320
EXIT_CODE_FAILURE = 1
@@ -39,10 +46,10 @@ class FakeConfig:
3946
Simple config for testing without reading a config file.
4047
"""
4148

42-
annotations: dict[str, str] = {}
49+
annotations: dict[str, t.Any] = {}
4350
annotation_regexes: list[str] = []
4451
annotation_tokens: list[str] = []
45-
groups: list[str] = []
52+
groups: t.Union[list[str], dict[str, list[str]]] = []
4653
echo = VerboseEcho()
4754

4855

@@ -51,13 +58,17 @@ class FakeSearch(BaseSearch):
5158
Simple test class for directly testing BaseSearch since it's abstract.
5259
"""
5360

54-
def search(self):
61+
def search(self) -> dict[str, list[dict[str, t.Any]]]:
5562
"""
5663
Override for abstract base method.
64+
65+
Returns:
66+
Empty dict to satisfy the abstract method requirement
5767
"""
68+
return {}
5869

5970

60-
def delete_report_files(file_extension):
71+
def delete_report_files(file_extension: str) -> None:
6172
"""
6273
Delete all files with the given extension from the test_reports directory.
6374
@@ -76,7 +87,7 @@ def delete_report_files(file_extension):
7687
pass
7788

7889

79-
def call_script(args_list, delete_test_reports=True, delete_test_docs=True):
90+
def call_script(args_list: Sequence[str], delete_test_reports: bool = True, delete_test_docs: bool = True) -> Result:
8091
"""
8192
Call the code_annotations script with the given params and a generic config file.
8293
@@ -108,11 +119,11 @@ def call_script(args_list, delete_test_reports=True, delete_test_docs=True):
108119

109120

110121
def call_script_isolated(
111-
args_list,
112-
test_filesystem_cb=None,
113-
test_filesystem_report_cb=None,
114-
fake_safelist_data="{}"
115-
):
122+
args_list: list[str],
123+
test_filesystem_cb: Callable[[], None] | None = None,
124+
test_filesystem_report_cb: Callable[[str], None] | None = None,
125+
fake_safelist_data: str = "{}"
126+
) -> Result:
116127
"""
117128
Call the code_annotations script with the given params and a generic config file.
118129
@@ -122,9 +133,6 @@ def call_script_isolated(
122133
cleared. Use this if you need access to non-report files in the temp filesystem.
123134
test_filesystem_report_cb: Callback function, called after the command is run, before the temp filesystem
124135
is cleared. Callback is called with the raw text contents of the report file.
125-
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.
128136
129137
Returns:
130138
click.testing.Result: Result from the `CliRunner.invoke()` call.
@@ -151,7 +159,9 @@ def call_script_isolated(
151159

152160
if test_filesystem_report_cb:
153161
try:
154-
report_file = re.search(r'Generating report to (.*)', result.output).groups()[0]
162+
report_match = re.search(r'Generating report to (.*)', result.output)
163+
assert report_match is not None
164+
report_file = report_match.groups()[0]
155165
with open(report_file) as f:
156166
report_contents = f.read()
157167

@@ -163,7 +173,7 @@ def call_script_isolated(
163173
return result
164174

165175

166-
def get_report_filename_from_output(output):
176+
def get_report_filename_from_output(output: str) -> str | None:
167177
"""
168178
Find the report filename in a find_static or find_django output and return it.
169179
@@ -172,9 +182,10 @@ def get_report_filename_from_output(output):
172182
173183
Returns:
174184
Filename of the found report, or None of no name is found
175-
176185
"""
186+
match = re.search(r'Generating report to (.*)', output)
187+
assert match is not None
177188
try:
178-
return re.search(r'Generating report to (.*)', output).groups()[0]
189+
return match.groups()[0]
179190
except IndexError:
180191
return None

tests/test_base.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
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()
14-
search = FakeSearch(config)
16+
search = FakeSearch(config) # type: ignore
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'],
2224
'group2': ['token2', 'foo']
2325
}
24-
search = FakeSearch(config)
26+
search = FakeSearch(config) # type: ignore
2527
assert search._get_group_for_token('foo') == 'group2' # pylint: disable=protected-access
2628

2729

@@ -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
"""
@@ -83,10 +85,10 @@ def test_format_results_for_report():
8385
'group2': ['token2', 'foo']
8486
}
8587

86-
search = FakeSearch(config)
88+
search = FakeSearch(config) # type: ignore
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: t.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)