Skip to content

Commit 64b4008

Browse files
committed
Refactor annotation checking to expose errors to pylint
Instead of just storing the error messages as part of `check_results()`, we also store the type of error. This allows us to expose these errors to pylint in edx-lint, with the help of a new checker. (to be created)
1 parent 70d076a commit 64b4008

File tree

5 files changed

+142
-66
lines changed

5 files changed

+142
-66
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ Change Log
1111

1212
.. There should always be an "Unreleased" section for changes pending release.
1313
14+
[1.1.0] - 2021-01-28
15+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16+
17+
* Refactor annotation checking to make it possible to expose errors via pylint
18+
1419
[1.0.2] - 2021-01-22
1520
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1621

code_annotations/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Extensible tools for parsing annotations in codebases.
33
"""
44

5-
__version__ = '1.0.2'
5+
__version__ = '1.1.0'
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
List possible annotation error types.
3+
"""
4+
from collections import namedtuple
5+
6+
AnnotationErrorType = namedtuple(
7+
"AnnotationError", ["message", "symbol", "description"]
8+
)
9+
10+
# The TYPES list should contain all AnnotationErrorType instances. This list can then be parsed by others, for instance
11+
# to expose errors to pylint.
12+
TYPES = []
13+
14+
15+
def add_error_type(message, symbol, description):
16+
"""
17+
Create an AnnotationErrorType instance and add it to TYPES.
18+
"""
19+
error_type = AnnotationErrorType(
20+
message,
21+
symbol,
22+
description,
23+
)
24+
TYPES.append(error_type)
25+
if len(TYPES) > 10:
26+
# if more than 10 items are created here, numerical IDs generated by edx-lint will overlap with other warning
27+
# IDs.
28+
raise ValueError("TYPES may not contain more than 10 items")
29+
return error_type
30+
31+
32+
# It is important to preserve the insertion order of these error types in the TYPES list, as edx-lint uses the error
33+
# type indices to generate numerical pylint IDs. If the insertion order is changed, the pylint IDs will change too,
34+
# which might cause incompatibilities down the road. Thus, new items should be added at the end.
35+
InvalidChoice = add_error_type(
36+
'"%s" is not a valid choice for "%s". Expected one of %s.',
37+
"annotation-invalid-choice",
38+
"Emitted when the value of a choice field is not one of the valid choices",
39+
)
40+
DuplicateChoiceValue = add_error_type(
41+
'"%s" is already present in this annotation.',
42+
"annotation-duplicate-choice-value",
43+
"Emitted when duplicate values are found in a choice field",
44+
)
45+
MissingChoiceValue = add_error_type(
46+
'no value found for "%s". Expected one of %s.',
47+
"annotation-missing-choice-value",
48+
"Emitted when a choice field does not have any value",
49+
)
50+
InvalidToken = add_error_type(
51+
"'%s' token does not belong to group '%s'. Expected one of: %s",
52+
"annotation-invalid-token",
53+
"Emitted when a token is found in a group for which it is not valid",
54+
)
55+
DuplicateToken = add_error_type(
56+
"found duplicate token '%s'",
57+
"annotation-duplicate-token",
58+
"Emitted when a token is found twice in a group",
59+
)
60+
MissingToken = add_error_type(
61+
"missing non-optional annotation: '%s'",
62+
"annotation-missing-token",
63+
"Emitted when a required token is missing from a group",
64+
)

code_annotations/base.py

Lines changed: 27 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import yaml
1111
from stevedore import named
1212

13+
from code_annotations import annotation_errors
1314
from code_annotations.exceptions import ConfigurationException
1415
from code_annotations.helpers import VerboseEcho
1516

@@ -314,7 +315,11 @@ def __init__(self, config):
314315
"""
315316
self.config = config
316317
self.echo = self.config.echo
318+
# errors contains formatted error messages
317319
self.errors = []
320+
# annotation_errors contains (annotation, AnnotationErrorType, args) tuples
321+
# This attribute may be parsed by 3rd-parties, such as edx-lint.
322+
self.annotation_errors = []
318323

319324
def format_file_results(self, all_results, results):
320325
"""
@@ -377,20 +382,18 @@ def _check_results_choices(self, annotation):
377382
if choice not in self.config.choices[token]:
378383
self._add_annotation_error(
379384
annotation,
380-
'"{}" is not a valid choice for "{}". Expected one of {}.'.format(
381-
choice,
382-
token,
383-
self.config.choices[token]
384-
)
385+
annotation_errors.InvalidChoice,
386+
(choice, token, self.config.choices[token])
385387
)
386388
elif choice in found_valid_choices:
387-
self._add_annotation_error(annotation, f'"{choice}" is already present in this annotation.')
389+
self._add_annotation_error(annotation, annotation_errors.DuplicateChoiceValue, (choice,))
388390
else:
389391
found_valid_choices.append(choice)
390392
else:
391393
self._add_annotation_error(
392394
annotation,
393-
'no value found for "{}". Expected one of {}.'.format(token, self.config.choices[token])
395+
annotation_errors.MissingChoiceValue,
396+
(token, self.config.choices[token])
394397
)
395398
return None
396399

@@ -502,7 +505,8 @@ def check_group(self, annotations):
502505
if token not in group_tokens:
503506
self._add_annotation_error(
504507
annotation,
505-
"'{}' token does not belong to group '{}'. Expected one of: {}".format(
508+
annotation_errors.InvalidToken,
509+
(
506510
token,
507511
group_name,
508512
group_tokens
@@ -513,7 +517,8 @@ def check_group(self, annotations):
513517
if token in found_tokens:
514518
self._add_annotation_error(
515519
annotation,
516-
"found duplicate token '{}'".format(token)
520+
annotation_errors.DuplicateToken,
521+
(token,)
517522
)
518523
if group_name:
519524
found_tokens.add(token)
@@ -524,69 +529,26 @@ def check_group(self, annotations):
524529
if token not in found_tokens:
525530
self._add_annotation_error(
526531
annotations[0],
527-
"missing non-optional annotation: '{}'".format(token)
528-
)
529-
530-
def check_annotation(self, annotation, current_group):
531-
"""
532-
Check an annotation and add annotation errors when necessary.
533-
534-
Args:
535-
annotation (dict): in particular, every annotation contains 'annotation_token' and 'annotation_data' keys.
536-
current_group (str): None or the name of a group (from self.config.groups) to which preceding annotations
537-
belong.
538-
found_group_tokens (list): annotation tokens from the same group that were already found. This list is
539-
cleared in case of error or when creating a new group.
540-
541-
Return:
542-
current_group (str or None)
543-
"""
544-
self._check_results_choices(annotation)
545-
token = annotation['annotation_token']
546-
547-
if current_group:
548-
# Add to existing group
549-
if token not in self.config.groups[current_group]:
550-
# Check for token correctness
551-
self._add_annotation_error(
552-
annotation,
553-
'"{}" is not in the group that starts with "{}". Expecting one of: {}'.format(
554-
token,
555-
current_group,
556-
self.config.groups[current_group]
532+
annotation_errors.MissingToken,
533+
(token,)
557534
)
558-
)
559-
current_group = None
560-
else:
561-
# Token is correct
562-
self.echo.echo_vv('Adding "{}", line {} to group {}'.format(
563-
token,
564-
annotation['line_number'],
565-
current_group
566-
))
567-
else:
568-
current_group = self._get_group_for_token(token)
569-
if current_group:
570-
# Start a new group
571-
self.echo.echo_vv('Starting new group for "{}" token "{}", line {}'.format(
572-
current_group, token, annotation['line_number'])
573-
)
574-
575-
return current_group
576535

577-
def _add_annotation_error(self, annotation, message):
536+
def _add_annotation_error(self, annotation, error_type, args=None):
578537
"""
579538
Add an error message to self.errors, formatted nicely.
580539
581540
Args:
582541
annotation: A single annotation dict found in search()
583-
message: Custom error message to be added
584-
"""
585-
if 'extra' in annotation and 'object_id' in annotation['extra']:
586-
error = "{}::{}: {}".format(annotation['filename'], annotation['extra']['object_id'], message)
587-
else:
588-
error = "{}::{}: {}".format(annotation['filename'], annotation['line_number'], message)
589-
self.errors.append(error)
542+
error_type (annotation_errors.AnnotationErrorType): error type from which the error message will be
543+
generated.
544+
args (tuple): arguments for error message formatting.
545+
"""
546+
args = args or tuple()
547+
error_message = error_type.message % args
548+
location = annotation.get("extra", {}).get("object_id", annotation["line_number"])
549+
message = "{}::{}: {}".format(annotation['filename'], location, error_message)
550+
self.annotation_errors.append((annotation, error_type, args))
551+
self._add_error(message)
590552

591553
def _add_error(self, message):
592554
"""

tests/test_search.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""
2+
Tests for the StaticSearch/DjangoSearch API.
3+
"""
4+
5+
from code_annotations import annotation_errors
6+
from code_annotations.base import AnnotationConfig
7+
from code_annotations.find_static import StaticSearch
8+
9+
10+
def test_annotation_errors():
11+
config = AnnotationConfig(
12+
"tests/test_configurations/.annotations_test",
13+
verbosity=-1,
14+
source_path_override="tests/extensions/python_test_files/choice_failures_1.pyt",
15+
)
16+
search = StaticSearch(config)
17+
results = search.search()
18+
search.check_results(results)
19+
20+
# The first error should be an invalid choice error
21+
annotation, error_type, args = search.annotation_errors[0]
22+
assert {
23+
"annotation_data": ["doesnotexist"],
24+
"annotation_token": ".. ignored:",
25+
"filename": "choice_failures_1.pyt",
26+
"found_by": "python",
27+
"line_number": 1,
28+
} == annotation
29+
assert annotation_errors.InvalidChoice == error_type
30+
assert (
31+
"doesnotexist",
32+
".. ignored:",
33+
["irrelevant", "terrible", "silly-silly"],
34+
) == args
35+
36+
37+
def test_annotation_errors_ordering():
38+
# You should modify the value below every time a new annotation error type is added.
39+
assert 6 == len(annotation_errors.TYPES)
40+
# The value below must not be modified, ever. The number of annotation error types should NEVER exceed 10. Read the
41+
# module docs for more information.
42+
assert len(annotation_errors.TYPES) < 10
43+
# This is just to check that the ordering of the annotation error types does not change. You should not change this
44+
# test, but eventually add your own below.
45+
assert annotation_errors.MissingToken == annotation_errors.TYPES[5]

0 commit comments

Comments
 (0)