Skip to content
Branch: master
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
269 lines (194 sloc) 7.69 KB
"""Parse arguments from command line and configuration files."""
import fnmatch
import os
import sys
import re
import logging
from argparse import ArgumentParser
from . import __version__
from .libs.inirama import Namespace
from .lint.extensions import LINTERS
#: A default checkers
DEFAULT_LINTERS = 'pycodestyle', 'pyflakes', 'mccabe'
CURDIR = os.getcwd()
CONFIG_FILES = 'pylava.ini', 'setup.cfg', 'tox.ini', 'pytest.ini'
#: The skip pattern
SKIP_PATTERN = re.compile(r'# *noqa\b', re.I).search
# Parse a modelines
MODELINE_RE = re.compile(r'^\s*#\s+(?:pylava:)\s*((?:[\w_]*=[^:\n\s]+:?)+)', re.I | re.M)
# Setup a logger
LOGGER = logging.getLogger('pylava')
LOGGER.propagate = False
STREAM = logging.StreamHandler(sys.stdout)
class _Default(object): # pylint: disable=too-few-public-methods
def __init__(self, value=None):
self.value = value
def __str__(self):
return str(self.value)
def __repr__(self):
return "<_Default [%s]>" % self.value
def split_csp_str(val):
""" Split comma separated string into unique values, keeping their order.
:returns: list of splitted values
seen = set()
values = val if isinstance(val, (list, tuple)) else val.strip().split(',')
return [x for x in values if x and not (x in seen or seen.add(x))]
def parse_linters(linters):
""" Initialize choosen linters.
:returns: list of inited linters
result = list()
for name in split_csp_str(linters):
linter = LINTERS.get(name)
if linter:
result.append((name, linter))
logging.warning("Linter `%s` not found.", name)
return result
def get_default_config_file(rootdir=None):
"""Search for configuration file."""
if rootdir is None:
for path in CONFIG_FILES:
path = os.path.join(rootdir, path)
if os.path.isfile(path) and os.access(path, os.R_OK):
return path
DEFAULT_CONFIG_FILE = get_default_config_file(CURDIR)
PARSER = ArgumentParser(description="Code audit tool for python.")
"paths", nargs='*', default=_Default([CURDIR]),
help="Paths to files or directories for code check.")
"--verbose", "-v", action='store_true', help="Verbose mode.")
PARSER.add_argument('--version', action='version',
version='%(prog)s ' + __version__)
"--format", "-f", default=_Default('pycodestyle'),
choices=['pep8', 'pycodestyle', 'pylint', 'parsable'],
help="Choose errors format (pycodestyle, pylint, parsable).")
"--select", "-s", default=_Default(''), type=split_csp_str,
help="Select errors and warnings. (comma-separated list)")
"--sort", default=_Default(''), type=split_csp_str,
help="Sort result by error types. Ex. E,W,D")
"--linters", "-l", default=_Default(','.join(DEFAULT_LINTERS)),
type=parse_linters, help=(
"Select linters. (comma-separated). Choices are %s."
% ','.join(s for s in LINTERS.keys())
"--ignore", "-i", default=_Default(''), type=split_csp_str,
help="Ignore errors and warnings. (comma-separated)")
"--skip", default=_Default(''),
type=lambda s: [re.compile(fnmatch.translate(p)) for p in s.split(',') if p],
help="Skip files by masks (comma-separated, Ex. */")
PARSER.add_argument("--report", "-r", help="Send report to file [REPORT]")
"--hook", action="store_true", help="Install Git (Mercurial) hook.")
"--async", action="store_true", dest='async_mode',
help="Enable async mode. Useful for checking a lot of files. "
"Unsupported with pylint.")
"--options", "-o", default=DEFAULT_CONFIG_FILE, metavar='FILE',
help="Specify configuration file. "
"Looks for {}, or {} in the current directory (default: {}).".format(
", ".join(CONFIG_FILES[:-1]), CONFIG_FILES[-1],
"--force", "-F", action='store_true', default=_Default(False),
help="Force code checking (if linter doesn't allow)")
"--abspath", "-a", action='store_true', default=_Default(False),
help="Use absolute paths in output.")
ACTIONS = dict((a.dest, a) for a in PARSER._actions) # pylint: disable=protected-access
def parse_options(args=None, config=True, rootdir=CURDIR, **overrides): # noqa
""" Parse options from command line and configuration files.
:return argparse.Namespace:
args = args or []
# Parse args from command string
options = PARSER.parse_args(args)
options.file_params = dict()
options.linters_params = dict()
# Compile options from ini
if config:
cfg = get_config(str(options.options), rootdir=rootdir)
for opt, val in cfg.default.items():
if opt == 'async':
opt = 'async_mode''Find option %s (%s)', opt, val)
passed_value = getattr(options, opt, _Default())
if isinstance(passed_value, _Default):
if opt == 'paths':
val = val.split()
setattr(options, opt, _Default(val))
# Parse file related options
for name, opts in cfg.sections.items():
if name == cfg.default_section:
if name.startswith('pylava'):
name = name[7:]
if name in LINTERS:
options.linters_params[name] = dict(opts)
mask = re.compile(fnmatch.translate(name))
options.file_params[mask] = dict(opts)
# Override options
_override_options(options, **overrides)
# Postprocess options
for name in options.__dict__:
value = getattr(options, name)
if isinstance(value, _Default):
setattr(options, name, process_value(name, value.value))
if options.async_mode and 'pylint' in options.linters:
LOGGER.warning('Can\'t parse code asynchronously with pylint enabled.')
options.async_mode = False
return options
def _override_options(options, **overrides):
"""Override options."""
for opt, val in overrides.items():
passed_value = getattr(options, opt, _Default())
if opt in ('ignore', 'select') and passed_value:
value = process_value(opt, passed_value.value)
value += process_value(opt, val)
setattr(options, opt, value)
elif isinstance(passed_value, _Default):
setattr(options, opt, process_value(opt, val))
def process_value(name, value):
""" Compile option value. """
action = ACTIONS.get(name)
if not action:
return value
if callable(action.type):
return action.type(value)
if action.const:
return bool(int(value))
return value
def get_config(ini_path=None, rootdir=None):
""" Load configuration from INI.
:return Namespace:
config = Namespace()
config.default_section = 'pylava'
if not ini_path or ini_path == 'None':
path = get_default_config_file(rootdir)
if path:
return config
def setup_logger(options):
"""Do the logger setup with options."""
LOGGER.setLevel(logging.INFO if options.verbose else logging.WARN)
LOGGER.addHandler(logging.FileHandler(, mode='w'))
if options.options:'Try to read configuration from: ' + options.options)
# pylava:ignore=W0212,D210,F0001
You can’t perform that action at this time.