Skip to content

Commit 315f9e1

Browse files
committed
Implement optional fields
This causes backward incompatibility, as we no longer accept annotation groups of the form: """ .. annotation1: ... """ # .. annotation2: ... In order to implement this feature, we had to refactor linting. `check_result()` was extremely convoluted; we both made it clearer and improved the wording of error messages. We took the opportunity to improve error messages.
1 parent 6b652d2 commit 315f9e1

8 files changed

Lines changed: 135 additions & 50 deletions

File tree

code_annotations/base.py

Lines changed: 105 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def __init__(self, config_file_path, report_path_override=None, verbosity=1, sou
3131
"""
3232
self.groups = {}
3333
self.choices = {}
34+
self.optional_groups = []
3435
self.annotation_tokens = []
3536
self.annotation_regexes = []
3637
self.mgr = None
@@ -108,19 +109,36 @@ def _is_choice_group(self, token_or_group):
108109
Returns:
109110
True if the type of the annotation is correct for a choice group, otherwise False
110111
"""
111-
return isinstance(token_or_group, dict)
112+
return isinstance(token_or_group, dict) and "choices" in token_or_group
113+
114+
def _is_optional_group(self, token_or_group):
115+
"""
116+
Determine if an annotation is an optional group.
117+
118+
Args:
119+
token_or_group: The annotation being checked
120+
121+
Returns:
122+
True if the annotation is optional, otherwise False.
123+
"""
124+
return isinstance(token_or_group, dict) and bool(token_or_group.get("optional"))
112125

113126
def _is_annotation_token(self, token_or_group):
114127
"""
115-
Determine if an annotation is a free-form text type.
128+
Determine if an annotation has the right format.
116129
117130
Args:
118131
token_or_group: The annotation being checked
119132
120133
Returns:
121134
True if the type of the annotation is correct for a text type, otherwise False
122135
"""
123-
return token_or_group is None
136+
if token_or_group is None:
137+
return True
138+
if isinstance(token_or_group, dict):
139+
# If annotation is a dict, only a few keys are tolerated
140+
return set(token_or_group.keys()).issubset({"choices", "optional"})
141+
return False
124142

125143
def _add_annotation_token(self, token):
126144
if token in self.annotation_tokens:
@@ -172,13 +190,15 @@ def _configure_group(self, group_name, group):
172190
for annotation_token in annotation:
173191
annotation_value = annotation[annotation_token]
174192

193+
# Otherwise it should be a text type, if not then error out
194+
if not self._is_annotation_token(annotation_value):
195+
raise ConfigurationException(f'{annotation} is an unknown annotation type.')
175196
# The annotation comment is a choice group
176197
if self._is_choice_group(annotation_value):
177198
self._configure_choices(annotation_token, annotation_value)
178-
179-
# Otherwise it should be a text type, if not then error out
180-
elif not self._is_annotation_token(annotation_value):
181-
raise ConfigurationException(f'{annotation} is an unknown annotation type.')
199+
# The annotation comment is not mandatory
200+
if self._is_optional_group(annotation_value):
201+
self.optional_groups.append(annotation_token)
182202

183203
self.groups[group_name].append(annotation_token)
184204
self._add_annotation_token(annotation_token)
@@ -370,7 +390,7 @@ def _check_results_choices(self, annotation):
370390
else:
371391
self._add_annotation_error(
372392
annotation,
373-
'No choices found for "{}". Expected one of {}.'.format(token, self.config.choices[token])
393+
'no value found for "{}". Expected one of {}.'.format(token, self.config.choices[token])
374394
)
375395
return None
376396

@@ -419,17 +439,86 @@ def check_results(self, all_results):
419439

420440
# Spin through the search results
421441
for filename in all_results:
422-
current_group = None
423-
found_group_tokens = []
424-
for annotation in all_results[filename]:
425-
current_group = self.check_annotation(annotation, current_group, found_group_tokens)
442+
for annotations in self.iter_groups(all_results[filename]):
443+
self.check_group(annotations)
444+
return not self.errors
426445

427-
if current_group:
428-
self.errors.append('File("{}") finished with an incomplete group {}!'.format(filename, current_group))
446+
def iter_groups(self, annotations):
447+
"""
448+
Iterate on groups of annotations. Annotations are considered as a group when they all have the same
449+
`line_number`, which should point to the beginning of the annotation group.
429450
430-
return not self.errors
451+
Yield:
452+
annotations (annotation list)
453+
"""
454+
current_group = []
455+
current_line_number = None
456+
for annotation in annotations:
457+
line_number = annotation["line_number"]
458+
line_number_changed = line_number != current_line_number
459+
if line_number_changed:
460+
if current_group:
461+
yield current_group
462+
current_group.clear()
463+
current_group.append(annotation)
464+
current_line_number = line_number
465+
466+
if current_group:
467+
yield current_group
431468

432-
def check_annotation(self, annotation, current_group, found_group_tokens):
469+
def check_group(self, annotations):
470+
"""
471+
Perform several linting checks on a group of annotations:
472+
473+
- Choice fields should have a valid value
474+
- Annotation tokens are valid
475+
- There is no duplicate
476+
- All non-optional tokens are present
477+
"""
478+
found_tokens = set()
479+
group_tokens = []
480+
group_name = None
481+
for annotation in annotations:
482+
token = annotation["annotation_token"]
483+
if not group_name:
484+
group_name = self._get_group_for_token(token)
485+
if group_name:
486+
group_tokens = self.config.groups[group_name]
487+
488+
# Check if choice field
489+
self._check_results_choices(annotation)
490+
491+
# Check token belongs to group
492+
if group_name:
493+
if token not in group_tokens:
494+
self._add_annotation_error(
495+
annotation,
496+
"'{}' token does not belong to group '{}'. Expected one of: {}".format(
497+
token,
498+
group_name,
499+
group_tokens
500+
)
501+
)
502+
503+
# Check for duplicates
504+
if token in found_tokens:
505+
self._add_annotation_error(
506+
annotation,
507+
"found duplicate token '{}'".format(token)
508+
)
509+
if group_name:
510+
found_tokens.add(token)
511+
512+
# Check for missing tokens
513+
for token in group_tokens:
514+
if token not in self.config.optional_groups:
515+
if token not in found_tokens:
516+
self._add_annotation_error(
517+
annotations[0],
518+
"missing non-optional annotation: '{}'".format(token)
519+
)
520+
521+
def check_annotation(self, annotation, current_group):
433522
"""
434523
Check an annotation and add annotation errors when necessary.
435524
@@ -459,39 +548,21 @@ def check_annotation(self, annotation, current_group, found_group_tokens):
459548
)
460549
)
461550
current_group = None
462-
found_group_tokens.clear()
463-
elif token in found_group_tokens:
464-
# Check for duplicate tokens
465-
self._add_annotation_error(
466-
annotation,
467-
'"{}" is already in the group that starts with "{}"'.format(token, current_group)
468-
)
469-
current_group = None
470-
found_group_tokens.clear()
471551
else:
472552
# Token is correct
473553
self.echo.echo_vv('Adding "{}", line {} to group {}'.format(
474554
token,
475555
annotation['line_number'],
476556
current_group
477557
))
478-
found_group_tokens.append(token)
479558
else:
480559
current_group = self._get_group_for_token(token)
481560
if current_group:
482561
# Start a new group
483-
found_group_tokens.clear()
484-
found_group_tokens.append(token)
485562
self.echo.echo_vv('Starting new group for "{}" token "{}", line {}'.format(
486563
current_group, token, annotation['line_number'])
487564
)
488565

489-
# If we have all members, this group is done
490-
if current_group and len(found_group_tokens) == len(self.config.groups[current_group]):
491-
self.echo.echo_vv("Group complete!")
492-
current_group = None
493-
found_group_tokens.clear()
494-
495566
return current_group
496567

497568
def _add_annotation_error(self, annotation, message):

docs/writing_annotations.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,15 @@ This comment also works even though the ordering is different:
222222
.. reporting_consumers: Recommendations and email marketing events
223223
"""
224224
225+
If some annotations in a group are optional, you should flag them as such::
226+
227+
.. code-block:: yaml
228+
229+
annotations:
230+
reporting:
231+
- ".. reporting:"
232+
- ".. reporting_non_required_field:":
233+
optional: true
234+
235+
Otherwise, linting will trigger an error complaining of a missing annotation when the
236+
"`.. reporting_non_required_field:`" annotation is not present.

tests/extensions/javascript_test_files/simple_success.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
/* .. pii: Group 1 - Annotation 1 */
2-
// .. pii_types: id, name
3-
// .. pii_retirement: local_api, consumer_api
1+
/* .. pii: Group 1 - Annotation 1
2+
.. pii_types: id, name
3+
.. pii_retirement: local_api, consumer_api
4+
*/
45

56
'use strict';
67

@@ -13,9 +14,9 @@ var bar = [
1314
/*
1415
.. pii: Group 2 - Annotation 1 comment is a
1516
multi line comment
17+
.. pii_types: id, name
18+
.. pii_retirement: local_api, consumer_api
1619
*/
17-
// .. pii_types: id, name
18-
// .. pii_retirement: local_api, consumer_api
1920
/pii\/js/,
2021
// .. pii: Group 3 - Annotation 1
2122
// .. pii_types: id, name

tests/extensions/python_test_files/simple_success.pyt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ Docstring
33
44
.. pii: Annotation 1
55
.. pii_types: id, name
6+
.. pii_retirement: local_api, consumer_api
67
"""
7-
# Should be able to finish the group outside of the same comment if necessary
8-
# .. pii_retirement: local_api, consumer_api
98

109

1110
def do():

tests/extensions/test_extension_javascript.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
('simple_success.js', EXIT_CODE_SUCCESS, 'Search found 26 annotations in'),
1111
('group_ordering_1.js', EXIT_CODE_SUCCESS, 'Search found 3 annotations in'),
1212
('group_ordering_2.js', EXIT_CODE_SUCCESS, 'Search found 9 annotations in'),
13-
('group_failures_1.js', EXIT_CODE_FAILURE, 'File("group_failures_1.js") finished with an incomplete group'),
14-
('group_failures_2.js', EXIT_CODE_FAILURE, 'File("group_failures_2.js") finished with an incomplete group'),
15-
('group_failures_4.js', EXIT_CODE_FAILURE, '".. no_pii:" is not in the group that starts with'),
16-
('group_failures_5.js', EXIT_CODE_FAILURE, '".. pii_types:" is already in the group that starts with'),
13+
('group_failures_1.js', EXIT_CODE_FAILURE, "missing non-optional annotation: '.. pii_retirement:'"),
14+
('group_failures_2.js', EXIT_CODE_FAILURE, "missing non-optional annotation: '.. pii:"),
15+
('group_failures_4.js', EXIT_CODE_FAILURE, "'.. no_pii:' token does not belong to group 'pii_group'"),
16+
('group_failures_5.js', EXIT_CODE_FAILURE, "found duplicate token '.. pii_types:'"),
1717
('choice_failures_1.js', EXIT_CODE_FAILURE, '"doesnotexist" is not a valid choice for ".. ignored:"'),
1818
('choice_failures_2.js', EXIT_CODE_FAILURE, '"doesnotexist" is not a valid choice for ".. ignored:"'),
1919
('choice_failures_3.js', EXIT_CODE_FAILURE, '"terrible|silly-silly" is not a valid choice for ".. ignored:"'),
2020
('choice_failures_4.js', EXIT_CODE_FAILURE, '"terrible" is already present in this annotation'),
21-
('choice_failures_5.js', EXIT_CODE_FAILURE, 'No choices found for ".. ignored:"'),
21+
('choice_failures_5.js', EXIT_CODE_FAILURE, 'no value found for ".. ignored:"'),
2222
])
2323
def test_grouping_and_choice_failures(test_file, expected_exit_code, expected_message):
2424
result = call_script((

tests/extensions/test_extension_python.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@
1313
('simple_success.pyt', EXIT_CODE_SUCCESS, 'Search found 20 annotations in'),
1414
('group_ordering_1.pyt', EXIT_CODE_SUCCESS, 'Search found 3 annotations in'),
1515
('group_ordering_2.pyt', EXIT_CODE_SUCCESS, 'Search found 9 annotations in'),
16-
('group_failures_1.pyt', EXIT_CODE_FAILURE, 'File("group_failures_1.pyt") finished with an incomplete group'),
17-
('group_failures_2.pyt', EXIT_CODE_FAILURE, 'File("group_failures_2.pyt") finished with an incomplete group'),
16+
('group_failures_1.pyt', EXIT_CODE_FAILURE, "missing non-optional annotation: '.. pii_retirement:'"),
17+
('group_failures_2.pyt', EXIT_CODE_FAILURE, "missing non-optional annotation: '.. pii:'"),
1818
('choice_failures_1.pyt', EXIT_CODE_FAILURE, '"doesnotexist" is not a valid choice for ".. ignored:"'),
1919
('choice_failures_2.pyt', EXIT_CODE_FAILURE, '"doesnotexist" is not a valid choice for ".. ignored:"'),
2020
('choice_failures_3.pyt', EXIT_CODE_FAILURE, '"terrible|silly-silly" is not a valid choice for ".. ignored:"'),
2121
('choice_failures_4.pyt', EXIT_CODE_FAILURE, '"terrible" is already present in this annotation'),
22-
('choice_failures_5.pyt', EXIT_CODE_FAILURE, 'No choices found for ".. ignored:"'),
22+
('choice_failures_5.pyt', EXIT_CODE_FAILURE, 'no value found for ".. ignored:"'),
2323
])
2424
def test_grouping_and_choice_failures(test_file, expected_exit_code, expected_message):
2525
result = call_script((

tests/test_configurations/.annotations_test

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ annotations:
1212
choices: [id, name, other]
1313
- ".. pii_retirement:":
1414
choices: [retained, local_api, consumer_api, third_party]
15+
- ".. pii_optional:":
16+
optional: true
1517
extensions:
1618
python:
1719
- pyt

tests/test_find_django.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ def test_find_django_ordering_error(**kwargs):
292292
)
293293

294294
assert result.exit_code == EXIT_CODE_FAILURE
295-
assert '".. no_pii:" is not in the group ' in result.output
295+
assert "missing non-optional annotation: '.. pii:'" in result.output
296296

297297

298298
@patch.multiple(

0 commit comments

Comments
 (0)