6
6
import os
7
7
import pprint
8
8
import re
9
+ from abc import ABCMeta , abstractmethod
9
10
10
11
import six
11
12
import yaml
12
- from stevedore import named
13
13
14
14
from code_annotations .exceptions import ConfigurationException
15
15
from code_annotations .helpers import VerboseEcho , read_configuration
16
16
17
17
18
- class StaticSearch (object ):
18
+ @six .add_metaclass (ABCMeta )
19
+ class BaseSearch (object ):
19
20
"""
20
- Handles static code searching for annotations .
21
+ Base class for searchers .
21
22
"""
22
23
23
- def __init__ (self , config , source_path , report_path , verbosity ):
24
+ def __init__ (self , config , report_path , verbosity ):
24
25
"""
25
26
Initialize for StaticSearch.
26
27
27
28
Args:
28
29
config: Configuration file path
29
- source_path: Directory to be searched for annotations
30
30
report_path: Directory to write the report file to
31
31
verbosity: Integer representing verbosity level (0-3)
32
32
"""
33
33
self .config = {}
34
34
self .errors = []
35
35
self .groups = {}
36
36
self .choices = {}
37
- self .mgr = None
38
37
39
38
# Global logger for this script, shared with extensions
40
39
self .echo = VerboseEcho ()
41
- self .configure (config , source_path , report_path , verbosity )
40
+ self .configure (config , report_path , verbosity )
42
41
43
- def configure (self , config_file_path , source_path , report_path , verbosity ):
42
+ def configure (self , config_file_path , report_path , verbosity ):
44
43
"""
45
44
Read the configuration file and handle command line overrides.
46
45
47
46
Args:
48
47
config_file_path: Location of the configuration file
49
- source_path: Path to the code to be searched
50
48
report_path: Directory where the report will be generated
51
49
verbosity: Integer indicating the runtime verbosity level
52
50
@@ -58,65 +56,21 @@ def configure(self, config_file_path, source_path, report_path, verbosity):
58
56
59
57
self .config = read_configuration (config_file_path )
60
58
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 :
65
60
raise ConfigurationException ('report_path not given and not in configuration file' )
66
61
67
- if source_path :
68
- self .config ['source_path' ] = source_path
69
-
70
62
if report_path :
71
63
self .config ['report_path' ] = report_path
72
64
73
65
self .config ['verbosity' ] = verbosity
74
66
self .echo .set_verbosity (verbosity )
75
67
76
- self .configure_extensions ()
77
68
self .configure_groups_and_choices ()
78
69
79
70
self .echo .echo_v ("Verbosity level set to {}" .format (verbosity ))
80
71
self .echo .echo_v ("Configuration:" )
81
72
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' ]))
120
74
121
75
def configure_groups_and_choices (self ):
122
76
"""
@@ -166,50 +120,6 @@ def configure_groups_and_choices(self):
166
120
self .echo .echo_v ("Groups configured: {}" .format (self .groups ))
167
121
self .echo .echo_v ("Choices configured: {}" .format (self .choices ))
168
122
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
-
213
123
def format_file_results (self , all_results , results ):
214
124
"""
215
125
Add all extensions' search results for a file to the overall results.
@@ -221,9 +131,7 @@ def format_file_results(self, all_results, results):
221
131
Returns:
222
132
None, modifies all_results
223
133
"""
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 :
227
135
if not annotations :
228
136
continue
229
137
@@ -259,13 +167,15 @@ def _check_results_choices(self, annotation):
259
167
Args:
260
168
annotation: A single search result dict.
261
169
"""
170
+ # Not a choice type of annotation, nothing to do
262
171
if annotation ['annotation_token' ] not in self .choices :
263
172
return
264
173
265
174
token = annotation ['annotation_token' ]
266
175
found_valid_choices = []
267
176
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.
269
179
if annotation ['annotation_data' ][0 ] != "" :
270
180
for choice in annotation ['annotation_data' ]:
271
181
if choice not in self .choices [token ]:
@@ -383,62 +293,30 @@ def _add_annotation_error(self, annotation, message):
383
293
annotation: A single annotation dict found in search()
384
294
message: Custom error message to be added
385
295
"""
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 )
387
300
self .errors .append (error )
388
301
389
- def _search_one_file (self , full_name , known_extensions , file_extensions_map , all_results ):
302
+ def _add_error (self , message ):
390
303
"""
391
- Perform an annotation search on a single file, using all extensions it is configured for .
304
+ Add an error message to self.errors .
392
305
393
306
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
398
308
"""
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 )
416
310
311
+ @abstractmethod
417
312
def search (self ):
418
313
"""
419
314
Walk the source tree, send known file types to extensions.
420
315
421
316
Returns:
422
- Filename of the generated report
317
+ Dict of {filename: annotations} for all files with found annotations.
423
318
"""
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
442
320
443
321
def report (self , all_results ):
444
322
"""
0 commit comments