Skip to content

Commit 8adf46d

Browse files
committed
Add django model annotation reporting
1 parent a52e9e0 commit 8adf46d

18 files changed

+1514
-239
lines changed

Diff for: .annotations_sample

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
source_path: ../devstack-docker/edx-platform
1+
source_path: ../
22
report_path: reports
3+
safelist_path: .pii_safe_list.yml
34
annotations:
45
pii:
56
- ".. pii::"

Diff for: .coveragerc

-4
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,4 @@ data_file = .coverage
44
source=code_annotations
55
omit =
66
test_settings
7-
*migrations*
8-
*admin.py
9-
*static*
10-
*templates*
117
code_annotations/extensions/tests/python_test_files/*

Diff for: code_annotations/find_annotations.py renamed to code_annotations/base.py

+24-146
Original file line numberDiff line numberDiff line change
@@ -6,47 +6,45 @@
66
import os
77
import pprint
88
import re
9+
from abc import ABCMeta, abstractmethod
910

1011
import six
1112
import yaml
12-
from stevedore import named
1313

1414
from code_annotations.exceptions import ConfigurationException
1515
from code_annotations.helpers import VerboseEcho, read_configuration
1616

1717

18-
class StaticSearch(object):
18+
@six.add_metaclass(ABCMeta)
19+
class BaseSearch(object):
1920
"""
20-
Handles static code searching for annotations.
21+
Base class for searchers.
2122
"""
2223

23-
def __init__(self, config, source_path, report_path, verbosity):
24+
def __init__(self, config, report_path, verbosity):
2425
"""
2526
Initialize for StaticSearch.
2627
2728
Args:
2829
config: Configuration file path
29-
source_path: Directory to be searched for annotations
3030
report_path: Directory to write the report file to
3131
verbosity: Integer representing verbosity level (0-3)
3232
"""
3333
self.config = {}
3434
self.errors = []
3535
self.groups = {}
3636
self.choices = {}
37-
self.mgr = None
3837

3938
# Global logger for this script, shared with extensions
4039
self.echo = VerboseEcho()
41-
self.configure(config, source_path, report_path, verbosity)
40+
self.configure(config, report_path, verbosity)
4241

43-
def configure(self, config_file_path, source_path, report_path, verbosity):
42+
def configure(self, config_file_path, report_path, verbosity):
4443
"""
4544
Read the configuration file and handle command line overrides.
4645
4746
Args:
4847
config_file_path: Location of the configuration file
49-
source_path: Path to the code to be searched
5048
report_path: Directory where the report will be generated
5149
verbosity: Integer indicating the runtime verbosity level
5250
@@ -58,65 +56,21 @@ def configure(self, config_file_path, source_path, report_path, verbosity):
5856

5957
self.config = read_configuration(config_file_path)
6058

61-
if not source_path and 'source_path' not in self.config:
62-
raise ConfigurationException('source_path not given and not in configuration file')
63-
64-
if not report_path and 'report_path' not in self.config:
59+
if 'report_path' not in self.config and not report_path:
6560
raise ConfigurationException('report_path not given and not in configuration file')
6661

67-
if source_path:
68-
self.config['source_path'] = source_path
69-
7062
if report_path:
7163
self.config['report_path'] = report_path
7264

7365
self.config['verbosity'] = verbosity
7466
self.echo.set_verbosity(verbosity)
7567

76-
self.configure_extensions()
7768
self.configure_groups_and_choices()
7869

7970
self.echo.echo_v("Verbosity level set to {}".format(verbosity))
8071
self.echo.echo_v("Configuration:")
8172
self.echo.echo_v(self.config)
82-
self.echo(
83-
"Configured for source path: {}, report path: {}".format(
84-
self.config['source_path'],
85-
self.config['report_path'])
86-
)
87-
88-
def configure_extensions(self):
89-
"""
90-
Configure the Stevedore NamedExtensionManager.
91-
92-
Raises:
93-
ConfigurationException
94-
"""
95-
# These are the names of all of our configured extensions
96-
configured_extension_names = self.config['extensions'].keys()
97-
98-
# Load Stevedore extensions that we are configured for (and only those)
99-
self.mgr = named.NamedExtensionManager(
100-
names=configured_extension_names,
101-
namespace='annotation_finder.searchers',
102-
invoke_on_load=True,
103-
on_load_failure_callback=self.load_failed_handler,
104-
invoke_args=(self.config, self.echo),
105-
)
106-
107-
# Output extension names listed in configuration
108-
self.echo.echo_vv("Configured extension names: {}".format(" ".join(configured_extension_names)))
109-
110-
# Output found extension entry points from setup.py|cfg (whether or not they were loaded)
111-
self.echo.echo_vv("Stevedore entry points found: {}".format(str(self.mgr.list_entry_points())))
112-
113-
# Output extensions that were actually able to load
114-
self.echo.echo_v("Loaded extensions: {}".format(" ".join([x.name for x in self.mgr.extensions])))
115-
116-
if len(self.mgr.extensions) != len(configured_extension_names):
117-
raise ConfigurationException('Not all configured extensions could be loaded! Asked for {} got {}.'.format(
118-
configured_extension_names, self.mgr.extensions
119-
))
73+
self.echo("Configured for report path: {}".format(self.config['report_path']))
12074

12175
def configure_groups_and_choices(self):
12276
"""
@@ -166,50 +120,6 @@ def configure_groups_and_choices(self):
166120
self.echo.echo_v("Groups configured: {}".format(self.groups))
167121
self.echo.echo_v("Choices configured: {}".format(self.choices))
168122

169-
def load_failed_handler(self, *args, **kwargs):
170-
"""
171-
Handle failures to load an extension.
172-
173-
Dumps the error and raises an exception. By default these
174-
errors just fail silently.
175-
176-
Args:
177-
*args:
178-
**kwargs:
179-
180-
Raises:
181-
ConfigurationException
182-
"""
183-
self.echo(args)
184-
self.echo(kwargs)
185-
raise ConfigurationException('Failed to load a plugin, aborting.')
186-
187-
def search_extension(self, ext, file_handle, file_extensions_map, filename_extension):
188-
"""
189-
Execute a search on the given file using the given extension.
190-
191-
Args:
192-
ext: Extension to execute the search on
193-
file_handle: An open file handle search
194-
file_extensions_map: Dict mapping of extension names to configured filename extensions
195-
filename_extension: The filename extension of the file being searched
196-
197-
Returns:
198-
Tuple of (extension name, list of found annotation dicts)
199-
"""
200-
# Only search this file if we are configured for its extension
201-
if filename_extension in file_extensions_map[ext.name]:
202-
# Reset the read handle to the beginning of the file in case another
203-
# extension moved it
204-
file_handle.seek(0)
205-
206-
ext_results = ext.obj.search(file_handle)
207-
208-
if ext_results:
209-
return ext.name, ext_results
210-
211-
return ext.name, None
212-
213123
def format_file_results(self, all_results, results):
214124
"""
215125
Add all extensions' search results for a file to the overall results.
@@ -221,9 +131,7 @@ def format_file_results(self, all_results, results):
221131
Returns:
222132
None, modifies all_results
223133
"""
224-
# "_" here is the extension name, as required by Stevedore map(). Each
225-
# annotation already has the extension name so we can ignore it
226-
for _, annotations in results:
134+
for annotations in results:
227135
if not annotations:
228136
continue
229137

@@ -259,13 +167,15 @@ def _check_results_choices(self, annotation):
259167
Args:
260168
annotation: A single search result dict.
261169
"""
170+
# Not a choice type of annotation, nothing to do
262171
if annotation['annotation_token'] not in self.choices:
263172
return
264173

265174
token = annotation['annotation_token']
266175
found_valid_choices = []
267176

268-
# If there are no choices on the line, split will return this
177+
# If the line begins with an annotation token that should have choices, but has no text after the token,
178+
# the first split will be empty.
269179
if annotation['annotation_data'][0] != "":
270180
for choice in annotation['annotation_data']:
271181
if choice not in self.choices[token]:
@@ -383,62 +293,30 @@ def _add_annotation_error(self, annotation, message):
383293
annotation: A single annotation dict found in search()
384294
message: Custom error message to be added
385295
"""
386-
error = "{}::{}: {}".format(annotation['filename'], annotation['line_number'], message)
296+
if 'extra' in annotation and 'object_id' in annotation['extra']:
297+
error = "{}::{}: {}".format(annotation['filename'], annotation['extra']['object_id'], message)
298+
else:
299+
error = "{}::{}: {}".format(annotation['filename'], annotation['line_number'], message)
387300
self.errors.append(error)
388301

389-
def _search_one_file(self, full_name, known_extensions, file_extensions_map, all_results):
302+
def _add_error(self, message):
390303
"""
391-
Perform an annotation search on a single file, using all extensions it is configured for.
304+
Add an error message to self.errors.
392305
393306
Args:
394-
full_name: Complete filename
395-
known_extensions: List of all file name extensions we are configured to work on
396-
file_extensions_map: Mapping of file name extensions to Stevedore extensions
397-
all_results: A dict of annotations returned from search()
307+
message: Custom error message to be added
398308
"""
399-
filename_extension = os.path.splitext(full_name)[1][1:]
400-
401-
if filename_extension not in known_extensions:
402-
self.echo.echo_vvv(
403-
"{} is not a known extension, skipping ({}).".format(filename_extension, full_name)
404-
)
405-
return
406-
407-
self.echo.echo_vvv(full_name)
408-
409-
# TODO: This should probably be a generator so we don't have to store all results in memory
410-
with open(full_name, 'r') as file_handle:
411-
# Call search_extension on all loaded extensions
412-
results = self.mgr.map(self.search_extension, file_handle, file_extensions_map, filename_extension)
413-
414-
# Format and add the results to our running full set
415-
self.format_file_results(all_results, results)
309+
self.errors.append(message)
416310

311+
@abstractmethod
417312
def search(self):
418313
"""
419314
Walk the source tree, send known file types to extensions.
420315
421316
Returns:
422-
Filename of the generated report
317+
Dict of {filename: annotations} for all files with found annotations.
423318
"""
424-
# Index the results by extension name
425-
file_extensions_map = {}
426-
known_extensions = set()
427-
for extension_name in self.config['extensions']:
428-
file_extensions_map[extension_name] = self.config['extensions'][extension_name]
429-
known_extensions.update(self.config['extensions'][extension_name])
430-
431-
all_results = {}
432-
433-
if os.path.isfile(self.config['source_path']):
434-
self._search_one_file(self.config['source_path'], known_extensions, file_extensions_map, all_results)
435-
else:
436-
for root, _, files in os.walk(self.config['source_path']):
437-
for filename in files:
438-
full_name = os.path.join(root, filename)
439-
self._search_one_file(full_name, known_extensions, file_extensions_map, all_results)
440-
441-
return all_results
319+
pass # pragma: no cover
442320

443321
def report(self, all_results):
444322
"""

0 commit comments

Comments
 (0)