1"""Parse arguments from command line and configuration files.""" 2import fnmatch 3import os 4import sys 5import re 6 7import logging 8from argparse import ArgumentParser 9 10from . import __version__ 11from .libs.inirama import Namespace 12from .lint.extensions import LINTERS 13 14#: A default checkers 15DEFAULT_LINTERS = 'pycodestyle', 'pyflakes', 'mccabe' 16 17CURDIR = os.getcwd() 18CONFIG_FILES = 'pylama.ini', 'setup.cfg', 'tox.ini', 'pytest.ini' 19 20#: The skip pattern 21SKIP_PATTERN = re.compile(r'# *noqa\b', re.I).search 22 23# Parse a modelines 24MODELINE_RE = re.compile( 25 r'^\s*#\s+(?:pylama:)\s*((?:[\w_]*=[^:\n\s]+:?)+)', 26 re.I | re.M) 27 28# Setup a logger 29LOGGER = logging.getLogger('pylama') 30LOGGER.propagate = False 31STREAM = logging.StreamHandler(sys.stdout) 32LOGGER.addHandler(STREAM) 33 34 35class _Default(object): # pylint: disable=too-few-public-methods 36 37 def __init__(self, value=None): 38 self.value = value 39 40 def __str__(self): 41 return str(self.value) 42 43 def __repr__(self): 44 return "<_Default [%s]>" % self.value 45 46 47def split_csp_str(val): 48 """ Split comma separated string into unique values, keeping their order. 49 50 :returns: list of splitted values 51 """ 52 seen = set() 53 values = val if isinstance(val, (list, tuple)) else val.strip().split(',') 54 return [x for x in values if x and not (x in seen or seen.add(x))] 55 56 57def parse_linters(linters): 58 """ Initialize choosen linters. 59 60 :returns: list of inited linters 61 62 """ 63 result = list() 64 for name in split_csp_str(linters): 65 linter = LINTERS.get(name) 66 if linter: 67 result.append((name, linter)) 68 else: 69 logging.warning("Linter `%s` not found.", name) 70 return result 71 72 73def get_default_config_file(rootdir=None): 74 """Search for configuration file.""" 75 if rootdir is None: 76 return DEFAULT_CONFIG_FILE 77 78 for path in CONFIG_FILES: 79 path = os.path.join(rootdir, path) 80 if os.path.isfile(path) and os.access(path, os.R_OK): 81 return path 82 83 84DEFAULT_CONFIG_FILE = get_default_config_file(CURDIR) 85 86 87PARSER = ArgumentParser(description="Code audit tool for python.") 88PARSER.add_argument( 89 "paths", nargs='*', default=_Default([CURDIR]), 90 help="Paths to files or directories for code check.") 91 92PARSER.add_argument( 93 "--verbose", "-v", action='store_true', help="Verbose mode.") 94 95PARSER.add_argument('--version', action='version', 96 version='%(prog)s ' + __version__) 97 98PARSER.add_argument( 99 "--format", "-f", default=_Default('pycodestyle'), 100 choices=['pep8', 'pycodestyle', 'pylint', 'parsable'], 101 help="Choose errors format (pycodestyle, pylint, parsable).") 102 103PARSER.add_argument( 104 "--select", "-s", default=_Default(''), type=split_csp_str, 105 help="Select errors and warnings. (comma-separated list)") 106 107PARSER.add_argument( 108 "--sort", default=_Default(''), type=split_csp_str, 109 help="Sort result by error types. Ex. E,W,D") 110 111PARSER.add_argument( 112 "--linters", "-l", default=_Default(','.join(DEFAULT_LINTERS)), 113 type=parse_linters, help=( 114 "Select linters. (comma-separated). Choices are %s." 115 % ','.join(s for s in LINTERS) 116 )) 117 118PARSER.add_argument( 119 "--ignore", "-i", default=_Default(''), type=split_csp_str, 120 help="Ignore errors and warnings. (comma-separated)") 121 122PARSER.add_argument( 123 "--skip", default=_Default(''), 124 type=lambda s: [re.compile(fnmatch.translate(p)) 125 for p in s.split(',') if p], 126 help="Skip files by masks (comma-separated, Ex. */messages.py)") 127 128PARSER.add_argument("--report", "-r", help="Send report to file [REPORT]") 129PARSER.add_argument( 130 "--hook", action="store_true", help="Install Git (Mercurial) hook.") 131 132PARSER.add_argument( 133 "--concurrent", "--async", action="store_true", 134 help="Enable async mode. Useful for checking a lot of files. " 135 "Unsupported with pylint.") 136 137PARSER.add_argument( 138 "--options", "-o", default=DEFAULT_CONFIG_FILE, metavar='FILE', 139 help="Specify configuration file. " 140 "Looks for {}, or {} in the current directory (default: {}).".format( 141 ", ".join(CONFIG_FILES[:-1]), CONFIG_FILES[-1], 142 DEFAULT_CONFIG_FILE)) 143 144PARSER.add_argument( 145 "--force", "-F", action='store_true', default=_Default(False), 146 help="Force code checking (if linter doesn't allow)") 147 148PARSER.add_argument( 149 "--abspath", "-a", action='store_true', default=_Default(False), 150 help="Use absolute paths in output.") 151 152 153ACTIONS = dict((a.dest, a) 154 for a in PARSER._actions) # pylint: disable=protected-access 155 156 157def parse_options(args=None, config=True, rootdir=CURDIR, **overrides): # noqa 158 """ Parse options from command line and configuration files. 159 160 :return argparse.Namespace: 161 162 """ 163 args = args or [] 164 165 # Parse args from command string 166 options = PARSER.parse_args(args) 167 options.file_params = dict() 168 options.linters_params = dict() 169 170 # Compile options from ini 171 if config: 172 cfg = get_config(str(options.options), rootdir=rootdir) 173 for opt, val in cfg.default.items(): 174 LOGGER.info('Find option %s (%s)', opt, val) 175 passed_value = getattr(options, opt, _Default()) 176 if isinstance(passed_value, _Default): 177 if opt == 'paths': 178 val = val.split() 179 if opt == 'skip': 180 val = fix_pathname_sep(val) 181 setattr(options, opt, _Default(val)) 182 183 # Parse file related options 184 for name, opts in cfg.sections.items(): 185 186 if name == cfg.default_section: 187 continue 188 189 if name.startswith('pylama'): 190 name = name[7:] 191 192 if name in LINTERS: 193 options.linters_params[name] = dict(opts) 194 continue 195 196 mask = re.compile(fnmatch.translate(fix_pathname_sep(name))) 197 options.file_params[mask] = dict(opts) 198 199 # Override options 200 _override_options(options, **overrides) 201 202 # Postprocess options 203 for name in options.__dict__: 204 value = getattr(options, name) 205 if isinstance(value, _Default): 206 setattr(options, name, process_value(name, value.value)) 207 208 if options.concurrent and 'pylint' in options.linters: 209 LOGGER.warning('Can\'t parse code asynchronously with pylint enabled.') 210 options.concurrent = False 211 212 return options 213 214 215def _override_options(options, **overrides): 216 """Override options.""" 217 for opt, val in overrides.items(): 218 passed_value = getattr(options, opt, _Default()) 219 if opt in ('ignore', 'select') and passed_value: 220 value = process_value(opt, passed_value.value) 221 value += process_value(opt, val) 222 setattr(options, opt, value) 223 elif isinstance(passed_value, _Default): 224 setattr(options, opt, process_value(opt, val)) 225 226 227def process_value(name, value): 228 """ Compile option value. """ 229 action = ACTIONS.get(name) 230 if not action: 231 return value 232 233 if callable(action.type): 234 return action.type(value) 235 236 if action.const: 237 return bool(int(value)) 238 239 return value 240 241 242def get_config(ini_path=None, rootdir=None): 243 """ Load configuration from INI. 244 245 :return Namespace: 246 247 """ 248 config = Namespace() 249 config.default_section = 'pylama' 250 251 if not ini_path: 252 path = get_default_config_file(rootdir) 253 if path: 254 config.read(path) 255 else: 256 config.read(ini_path) 257 258 return config 259 260 261def setup_logger(options): 262 """Do the logger setup with options.""" 263 LOGGER.setLevel(logging.INFO if options.verbose else logging.WARN) 264 if options.report: 265 LOGGER.removeHandler(STREAM) 266 LOGGER.addHandler(logging.FileHandler(options.report, mode='w')) 267 268 if options.options: 269 LOGGER.info('Try to read configuration from: %r', options.options) 270 271 272def fix_pathname_sep(val): 273 """Fix pathnames for Win.""" 274 return val.replace(os.altsep or "\\", os.sep) 275 276# pylama:ignore=W0212,D210,F0001 277