Skip to content

Commit abaae47

Browse files
committed
chore: strict type annotations of test directories
1 parent 74db00e commit abaae47

13 files changed

+172
-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: 41 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,41 @@
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+
def __init__(self) -> None: # pylint: disable=super-init-not-called
51+
"""
52+
Override the base __init__ to skip reading and parsing the config.
53+
"""
54+
self.groups: dict[str, list[str]] = {}
55+
self.choices: dict[str, list[str]] = {}
56+
self.optional_groups: list[str] = []
57+
self.annotation_tokens: list[str] = []
58+
self.annotation_regexes: list[str] = []
59+
self.mgr: named.NamedExtensionManager | None = None
60+
self.coverage_target: float | None = None
61+
self.echo = VerboseEcho()
4762

4863

4964
class FakeSearch(BaseSearch):
5065
"""
5166
Simple test class for directly testing BaseSearch since it's abstract.
5267
"""
5368

54-
def search(self):
69+
def search(self) -> dict[str, list[dict[str, t.Any]]]:
5570
"""
5671
Override for abstract base method.
72+
73+
Returns:
74+
Empty dict to satisfy the abstract method requirement
5775
"""
76+
return {}
5877

5978

60-
def delete_report_files(file_extension):
79+
def delete_report_files(file_extension: str) -> None:
6180
"""
6281
Delete all files with the given extension from the test_reports directory.
6382
@@ -76,7 +95,7 @@ def delete_report_files(file_extension):
7695
pass
7796

7897

79-
def call_script(args_list, delete_test_reports=True, delete_test_docs=True):
98+
def call_script(args_list: Sequence[str], delete_test_reports: bool = True, delete_test_docs: bool = True) -> Result:
8099
"""
81100
Call the code_annotations script with the given params and a generic config file.
82101
@@ -108,11 +127,11 @@ def call_script(args_list, delete_test_reports=True, delete_test_docs=True):
108127

109128

110129
def call_script_isolated(
111-
args_list,
112-
test_filesystem_cb=None,
113-
test_filesystem_report_cb=None,
114-
fake_safelist_data="{}"
115-
):
130+
args_list: list[str],
131+
test_filesystem_cb: Callable[[], None] | None = None,
132+
test_filesystem_report_cb: Callable[[str], None] | None = None,
133+
fake_safelist_data: str = "{}"
134+
) -> Result:
116135
"""
117136
Call the code_annotations script with the given params and a generic config file.
118137
@@ -123,8 +142,6 @@ def call_script_isolated(
123142
test_filesystem_report_cb: Callback function, called after the command is run, before the temp filesystem
124143
is cleared. Callback is called with the raw text contents of the report file.
125144
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.
128145
129146
Returns:
130147
click.testing.Result: Result from the `CliRunner.invoke()` call.
@@ -151,7 +168,9 @@ def call_script_isolated(
151168

152169
if test_filesystem_report_cb:
153170
try:
154-
report_file = re.search(r'Generating report to (.*)', result.output).groups()[0]
171+
report_match = re.search(r'Generating report to (.*)', result.output)
172+
assert report_match is not None
173+
report_file = report_match.groups()[0]
155174
with open(report_file) as f:
156175
report_contents = f.read()
157176

@@ -163,7 +182,7 @@ def call_script_isolated(
163182
return result
164183

165184

166-
def get_report_filename_from_output(output):
185+
def get_report_filename_from_output(output: str) -> str | None:
167186
"""
168187
Find the report filename in a find_static or find_django output and return it.
169188
@@ -172,9 +191,10 @@ def get_report_filename_from_output(output):
172191
173192
Returns:
174193
Filename of the found report, or None of no name is found
175-
176194
"""
195+
match = re.search(r'Generating report to (.*)', output)
196+
assert match is not None
177197
try:
178-
return re.search(r'Generating report to (.*)', output).groups()[0]
198+
return match.groups()[0]
179199
except IndexError:
180200
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 = 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(),

tests/test_django_generate_safelist.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Tests for seeding the safelist.
44
"""
55
import os
6+
import typing as t
67
from unittest.mock import DEFAULT, MagicMock, patch
78

89
import pytest
@@ -34,19 +35,23 @@
3435
[], # No non-local models to add to the safelist.
3536
),
3637
])
37-
def test_seeding_safelist(local_models, non_local_models, **kwargs):
38+
def test_seeding_safelist(
39+
local_models: list[MagicMock],
40+
non_local_models: list[MagicMock],
41+
**kwargs: t.Any
42+
) -> None:
3843
"""
3944
Test the success case for seeding the safelist.
4045
"""
41-
mock_get_models_requiring_annotations = kwargs['get_models_requiring_annotations']
46+
mock_get_models_requiring_annotations: MagicMock = kwargs['get_models_requiring_annotations']
4247
mock_get_models_requiring_annotations.return_value = (
4348
local_models,
4449
non_local_models,
4550
0, # Number of total models found, irrelevant here
4651
set() # List of model ids that are eligible for annotation, irrelevant here
4752
)
4853

49-
def test_safelist_callback():
54+
def test_safelist_callback() -> None:
5055
assert os.path.exists(DEFAULT_FAKE_SAFELIST_PATH)
5156
with open(DEFAULT_FAKE_SAFELIST_PATH) as fake_safelist_file:
5257
fake_safelist = fake_safelist_file.read()
@@ -58,7 +63,7 @@ def test_safelist_callback():
5863
result = call_script_isolated(
5964
['django_find_annotations', '--config_file', 'test_config.yml', '--seed_safelist'],
6065
test_filesystem_cb=test_safelist_callback,
61-
fake_safelist_data=None
66+
fake_safelist_data=""
6267
)
6368
assert result.exit_code == EXIT_CODE_SUCCESS
6469
assert 'Successfully created safelist file' in result.output
@@ -68,11 +73,11 @@ def test_safelist_callback():
6873
'code_annotations.find_django.DjangoSearch',
6974
get_models_requiring_annotations=DEFAULT,
7075
)
71-
def test_safelist_exists(**kwargs):
76+
def test_safelist_exists(**kwargs: t.Any) -> None:
7277
"""
7378
Test the success case for seeding the safelist.
7479
"""
75-
mock_get_models_requiring_annotations = kwargs['get_models_requiring_annotations']
80+
mock_get_models_requiring_annotations: MagicMock = kwargs['get_models_requiring_annotations']
7681
mock_get_models_requiring_annotations.return_value = (set(), set(), 0, [])
7782

7883
result = call_script_isolated(

0 commit comments

Comments
 (0)