66import re
77
88import logging
9+ from collections import defaultdict
910
1011from .config import process_value , LOGGER
1112from .lint .extensions import LINTERS
13+ from .errors import DUPLICATES , Error
1214
1315
1416#: The skip pattern
2022 re .I | re .M )
2123
2224
23- def run (path , code = None , options = None ):
25+ def run (path = '' , code = None , options = None ):
2426 """ Run a code checkers with given params.
2527
2628 :return errors: list of dictionaries with error's information
2729
2830 """
2931 errors = []
30- params = dict (ignore = options .ignore , select = options .select )
3132 fileconfig = dict ()
32- for mask in options .file_params :
33- if mask .match (path ):
34- fileconfig .update (options .file_params [mask ])
33+ params = dict ()
34+ linters = LINTERS
35+ linter_params = dict ()
36+
37+ if options :
38+ linters = options .linters
39+ linter_params = options .linter_params
40+ for mask in options .file_params :
41+ if mask .match (path ):
42+ fileconfig .update (options .file_params [mask ])
3543
3644 try :
3745 with CodeContext (code , path ) as ctx :
@@ -41,51 +49,43 @@ def run(path, code=None, options=None):
4149 if params .get ('skip' ):
4250 return errors
4351
44- for item in options . linters :
52+ for item in linters :
4553
4654 if not isinstance (item , tuple ):
4755 item = (item , LINTERS .get (item ))
4856
49- name , linter = item
57+ lname , linter = item
5058
51- if not linter or not linter .allow (path ):
59+ if not linter or path and not linter .allow (path ):
5260 continue
5361
54- LOGGER .info ("Run %s" , name )
55- meta = options .linter_params .get (name , dict ())
56- result = linter .run (path , code = code , ** meta )
57- for e in result :
58- e ['linter' ] = name
59- e ['col' ] = e .get ('col' ) or 0
60- e ['lnum' ] = e .get ('lnum' ) or 0
61- e ['type' ] = e .get ('type' ) or 'E'
62- e ['text' ] = "%s [%s]" % (
63- e .get ('text' , '' ).strip ().split ('\n ' )[0 ], name )
64- e ['filename' ] = path or ''
65- errors .append (e )
62+ LOGGER .info ("Run %s" , lname )
63+ meta = linter_params .get (lname , dict ())
64+ errors += [Error (filename = path , linter = lname , ** e )
65+ for e in linter .run (path , code = code , ** meta )]
6666
6767 except IOError as e :
6868 LOGGER .debug ("IOError %s" , e )
69- errors .append (dict (
70- lnum = 0 , type = 'E' , col = 0 , text = str (e ), filename = path or '' ))
69+ errors .append (Error (text = str (e ), filename = path , linter = lname ))
7170
7271 except SyntaxError as e :
7372 LOGGER .debug ("SyntaxError %s" , e )
74- errors .append (dict (
75- lnum = e .lineno or 0 , type = 'E' , col = e .offset or 0 ,
76- text = e .args [0 ] + ' [%s]' % name , filename = path or ''
77- ))
73+ errors .append (
74+ Error (linter = lname , lnum = e .lineno , col = e .offset , text = e .args [0 ],
75+ filename = path ))
7876
7977 except Exception as e :
8078 import traceback
8179 LOGGER .info (traceback .format_exc ())
8280
83- errors = [er for er in errors if filter_errors (er , ** params )]
81+ errors = filter_errors (errors , ** params )
82+
83+ errors = list (remove_duplicates (errors ))
8484
8585 if code and errors :
8686 errors = filter_skiplines (code , errors )
8787
88- return sorted (errors , key = lambda x : x [ ' lnum' ] )
88+ return sorted (errors , key = lambda e : e . lnum )
8989
9090
9191def parse_modeline (code ):
@@ -107,7 +107,10 @@ def prepare_params(modeline, fileconfig, options):
107107 :return dict:
108108
109109 """
110- params = dict (ignore = options .ignore , select = options .select , skip = False )
110+ params = dict (skip = False , ignore = [], select = [])
111+ if options :
112+ params ['ignore' ] = options .ignore
113+ params ['select' ] = options .select
111114
112115 for config in filter (None , [modeline , fileconfig ]):
113116 for key in ('ignore' , 'select' ):
@@ -120,23 +123,26 @@ def prepare_params(modeline, fileconfig, options):
120123 return params
121124
122125
123- def filter_errors (e , select = None , ignore = None , ** params ):
126+ def filter_errors (errors , select = None , ignore = None , ** params ):
124127 """ Filter a erros by select and ignore options.
125128
126129 :return bool:
127130
128131 """
129- if select :
130- for s in select :
131- if e ['text' ].startswith (s ):
132- return True
133-
134- if ignore :
135- for s in ignore :
136- if e ['text' ].startswith (s ):
137- return False
132+ select = select or []
133+ ignore = ignore or []
138134
139- return True
135+ for e in errors :
136+ for s in select :
137+ if e .number .startswith (s ):
138+ yield e
139+ break
140+ else :
141+ for s in ignore :
142+ if e .number .startswith (s ):
143+ break
144+ else :
145+ yield e
140146
141147
142148def filter_skiplines (code , errors ):
@@ -148,18 +154,30 @@ def filter_skiplines(code, errors):
148154 if not errors :
149155 return errors
150156
151- enums = set (er [ ' lnum' ] for er in errors )
157+ enums = set (er . lnum for er in errors )
152158 removed = set ([
153159 num for num , l in enumerate (code .split ('\n ' ), 1 )
154160 if num in enums and SKIP_PATTERN (l )
155161 ])
156162
157163 if removed :
158- errors = [er for er in errors if not er [ ' lnum' ] in removed ]
164+ errors = [er for er in errors if er . lnum not in removed ]
159165
160166 return errors
161167
162168
169+ def remove_duplicates (errors ):
170+ """ Remove same errors from others linters. """
171+ passed = defaultdict (list )
172+ for error in errors :
173+ key = error .linter , error .number
174+ if key in DUPLICATES :
175+ if key in passed [error .lnum ]:
176+ continue
177+ passed [error .lnum ] = DUPLICATES [key ]
178+ yield error
179+
180+
163181class CodeContext (object ):
164182
165183 """ Read file if code is None. """
0 commit comments