blob: ca783490ee51eb4d017886e26cfe439ce74a942b [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright (c) 2006-2010, 2012-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
# Copyright (c) 2008 pyves@crater.logilab.fr <pyves@crater.logilab.fr>
# Copyright (c) 2010 Julien Jehannet <julien.jehannet@logilab.fr>
# Copyright (c) 2013 Google, Inc.
# Copyright (c) 2013 John McGehee <jmcgehee@altera.com>
# Copyright (c) 2014-2018 Claudiu Popa <pcmanticore@gmail.com>
# Copyright (c) 2014 Brett Cannon <brett@python.org>
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
# Copyright (c) 2015 Aru Sahni <arusahni@gmail.com>
# Copyright (c) 2015 John Kirkham <jakirkham@gmail.com>
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
# Copyright (c) 2016 Erik <erik.eriksson@yahoo.com>
# Copyright (c) 2016 Alexander Todorov <atodorov@otb.bg>
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
# Copyright (c) 2017-2018 Ville Skyttä <ville.skytta@iki.fi>
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
# Copyright (c) 2017 ahirnish <ahirnish@gmail.com>
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
# Copyright (c) 2018 Bryce Guinta <bryce.paul.guinta@gmail.com>
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
# Copyright (c) 2018 Anthony Sottile <asottile@umich.edu>
# Copyright (c) 2018 Gary Tyler McLeod <mail@garytyler.com>
# Copyright (c) 2018 Konstantin <Github@pheanex.de>
# Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.com>
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
"""utilities for Pylint configuration :
* pylintrc
* pylint.d (PYLINTHOME)
"""
from __future__ import print_function
import abc
import argparse
import collections
import copy
import os
import pickle
import re
import sys
import textwrap
import configparser
from pylint import exceptions, utils
USER_HOME = os.path.expanduser("~")
if "PYLINTHOME" in os.environ:
PYLINT_HOME = os.environ["PYLINTHOME"]
if USER_HOME == "~":
USER_HOME = os.path.dirname(PYLINT_HOME)
elif USER_HOME == "~":
PYLINT_HOME = ".pylint.d"
else:
PYLINT_HOME = os.path.join(USER_HOME, ".pylint.d")
def _get_pdata_path(base_name, recurs):
base_name = base_name.replace(os.sep, "_")
return os.path.join(PYLINT_HOME, "%s%s%s" % (base_name, recurs, ".stats"))
def load_results(base):
data_file = _get_pdata_path(base, 1)
try:
with open(data_file, _PICK_LOAD) as stream:
return pickle.load(stream)
except Exception: # pylint: disable=broad-except
return {}
if sys.version_info < (3, 0):
_PICK_DUMP, _PICK_LOAD = "w", "r"
else:
_PICK_DUMP, _PICK_LOAD = "wb", "rb"
def save_results(results, base):
if not os.path.exists(PYLINT_HOME):
try:
os.mkdir(PYLINT_HOME)
except OSError:
print("Unable to create directory %s" % PYLINT_HOME, file=sys.stderr)
data_file = _get_pdata_path(base, 1)
try:
with open(data_file, _PICK_DUMP) as stream:
pickle.dump(results, stream)
except (IOError, OSError) as ex:
print("Unable to create file %s: %s" % (data_file, ex), file=sys.stderr)
def find_pylintrc_in(search_dir):
"""Find a pylintrc file in the given directory.
:param search_dir: The directory to search.
:type search_dir: str
:returns: The path to the pylintrc file, if found. Otherwise None.
:rtype: str or None
"""
path = None
search_dir = os.path.expanduser(search_dir)
if os.path.isfile(os.path.join(search_dir, "pylintrc")):
path = os.path.join(search_dir, "pylintrc")
elif os.path.isfile(os.path.join(search_dir, ".pylintrc")):
path = os.path.join(search_dir, ".pylintrc")
return path
def find_nearby_pylintrc(search_dir=""):
"""Search for the nearest pylint rc file.
:param search_dir: The directory to search.
:type search_dir: str
:returns: The absolute path to the pylintrc file, if found.
Otherwise None
:rtype: str or None
"""
search_dir = os.path.expanduser(search_dir)
path = find_pylintrc_in(search_dir)
if not path:
for cur_dir in utils.walk_up(search_dir):
if not os.path.isfile(os.path.join(cur_dir, "__init__.py")):
break
path = find_pylintrc_in(cur_dir)
if path:
break
if path:
path = os.path.abspath(path)
return path
def find_global_pylintrc():
"""Search for the global pylintrc file.
:returns: The absolute path to the pylintrc file, if found. Otherwise None.
:rtype: str or None
"""
pylintrc = None
if "PYLINTRC" in os.environ and os.path.isfile(os.environ["PYLINTRC"]):
pylintrc = os.environ["PYLINTRC"]
else:
search_dirs = ("~", "/root", os.path.join("~", ".config"), "/etc/pylintrc")
for search_dir in search_dirs:
path = find_pylintrc_in(search_dir)
if path:
pylintrc = path
break
return pylintrc
def find_pylintrc():
"""Search for a pylintrc file.
The locations searched are, in order:
- The current directory
- Each parent directory that contains a __init__.py file
- The value of the `PYLINTRC` environment variable
- The current user's home directory
- The `.config` folder in the current user's home directory
- /etc/pylintrc
:returns: The path to the pylintrc file, or None if one was not found.
:rtype: str or None
"""
# TODO: Find nearby pylintrc files as well
# return find_nearby_pylintrc() or find_global_pylintrc()
return find_global_pylintrc()
PYLINTRC = find_pylintrc()
ENV_HELP = (
"""
The following environment variables are used:
* PYLINTHOME
Path to the directory where persistent data for the run will be stored. If
not found, it defaults to ~/.pylint.d/ or .pylint.d (in the current working
directory).
* PYLINTRC
Path to the configuration file. See the documentation for the method used
to search for configuration file.
"""
% globals() # type: ignore
)
class UnsupportedAction(Exception):
"""raised by set_option when it doesn't know what to do for an action"""
def _regexp_csv_validator(value):
return [re.compile(val) for val in utils._check_csv(value)]
def _yn_validator(value):
if value in ("y", "yes"):
return True
if value in ("n", "no"):
return False
msg = "invalid yn value %r, should be in (y, yes, n, no)"
raise argparse.ArgumentTypeError(msg % (value,))
def _non_empty_string_validator(value):
if not value:
msg = "indent string can't be empty."
raise argparse.ArgumentTypeError(msg)
return utils._unquote(value)
VALIDATORS = {
"string": utils._unquote,
"int": int,
"regexp": re.compile,
"regexp_csv": _regexp_csv_validator,
"csv": utils._check_csv,
"yn": _yn_validator,
"non_empty_string": _non_empty_string_validator,
"_msg_on": (lambda value: (utils._check_csv(value), True)),
"_msg_off": (lambda value: (utils._check_csv(value), False)),
}
UNVALIDATORS = {
"string": str,
"int": str,
"regexp": lambda value: getattr(value, "pattern", value),
"regexp_csv": (lambda value: ",".join(r.pattern for r in value)),
"csv": (lambda value: ",".join(value)),
"yn": (lambda value: "y" if value else "n"),
"non_empty_string": str,
"_msg_on": (lambda value: ",".join(y for y in x[0] for x in value)),
"_msg_off": (lambda value: ",".join(y for y in x[0] for x in value)),
}
OptionDefinition = collections.namedtuple("OptionDefinition", ["name", "definition"])
class Configuration(object):
def __init__(self):
self._option_definitions = {}
def add_option(self, option_definition):
name, definition = option_definition
if name in self._option_definitions:
raise exceptions.ConfigurationError('Option "{0}" already exists.')
self._option_definitions[name] = definition
if "default" in definition:
dest = definition.get("dest", name)
self.set_option(dest, definition["default"])
def add_options(self, option_definitions):
for option_definition in option_definitions:
self.add_option(option_definition)
def set_option(self, option, value):
option = option.replace("-", "_")
definition = self._option_definitions.get(option, {})
dest = definition.get("dest", option)
if definition.get("action") == "append":
new_value = getattr(self, dest, [])
new_value.append(value)
value = new_value
setattr(self, dest, value)
def copy(self):
result = self.__class__()
result.add_options(self._option_definitions.items())
for option in self._option_definitions:
if hasattr(self, option):
value = getattr(self, option)
setattr(result, option, value)
return result
def __add__(self, other):
result = self.copy()
result += other
return result
def __iadd__(self, other):
self._option_definitions.update(other._option_definitions)
copied = set()
for option, definition in self._option_definitions.items():
option = option.replace("-", "_")
dest = definition.get("dest", option)
if dest not in copied and hasattr(other, dest):
value = getattr(other, dest)
if definition.get("action") == "append":
value = getattr(self, dest, []) + value
setattr(self, dest, value)
copied.add(dest)
return self
class ConfigurationStore(object):
def __init__(self):
"""A class to store configuration objects for many paths."""
self._store = {}
def add_config_for(self, path, config):
"""Add a configuration object to the store.
:param path: The path to add the config for.
:type path: str
:param config: The config object for the given path.
:type config: Configuration
"""
path = os.path.expanduser(path)
path = os.path.abspath(path)
self._store[path] = config
def get_config_for(self, path):
"""Get the configuration object for a file or directory.
:param path: The file or directory to the get configuration object for.
:type path: str
:returns: The configuration object for the given file or directory.
:rtype: Configuration or None
"""
path = os.path.expanduser(path)
path = os.path.abspath(path)
return self._store.get(path)
def __getitem__(self, path):
return self.get_config_for(path)
def __setitem__(self, path, config):
return self.add_config_for(path, config)
class ConfigParser(metaclass=abc.ABCMeta):
def __init__(self):
self._option_definitions = collections.OrderedDict()
self._option_groups = set()
def add_option_definitions(self, option_definitions):
self._option_definitions.update(option_definitions)
for _, definition_dict in option_definitions:
try:
group = definition_dict["group"].upper()
except KeyError:
continue
else:
self._option_groups.add(group)
def add_option_definition(self, option_definition):
self.add_option_definitions([option_definition])
@abc.abstractmethod
def parse(self, to_parse, config):
"""Parse the given object into the config object.
:param to_parse: The object to parse.
:type to_parse: object
:param config: The config object to parse into.
:type config: Configuration
"""
class CLIParser(ConfigParser):
def __init__(self, usage=""):
super(CLIParser, self).__init__()
self._parser = LongHelpArgumentParser(
usage=usage.replace("%prog", "%(prog)s"),
# Only set the arguments that are specified.
argument_default=argparse.SUPPRESS,
)
def add_option_definitions(self, option_definitions):
self._option_definitions.update(option_definitions)
option_groups = collections.defaultdict(list)
for option, definition in option_definitions:
group, args, kwargs = self._convert_definition(option, definition)
option_groups[group].append((args, kwargs))
for args, kwargs in option_groups["MASTER"]:
self._parser.add_argument(*args, **kwargs)
del option_groups["MASTER"]
for group, arguments in option_groups.items():
self._option_groups.add(group)
group = self._parser.add_argument_group(group.title())
for args, kwargs in arguments:
group.add_argument(*args, **kwargs)
@staticmethod
def _convert_definition(option, definition):
"""Convert an option definition to a set of arguments for add_argument.
:param option: The name of the option
:type option: str
:param definition: The argument definition to convert.
:type definition: dict
:returns: A tuple of the group to add the argument to,
plus the args and kwargs for :func:`ArgumentParser.add_argument`.
:rtype: tuple(str, list, dict)
:raises ConfigurationError: When the definition is invalid.
"""
args = []
if "short" in definition:
args.append("-{0}".format(definition["short"]))
if definition.get("positional", False):
args.append(option)
else:
args.append("--{0}".format(option))
copy_keys = (
"action",
"default",
"dest",
"help",
"metavar",
"level",
"version",
"nargs",
)
kwargs = {k: definition[k] for k in copy_keys if k in definition}
if "type" in definition:
if definition["type"] in VALIDATORS:
kwargs["type"] = VALIDATORS[definition["type"]]
elif definition["type"] in ("choice", "multiple_choice"):
if "choices" not in definition:
msg = 'No choice list given for option "{0}" of type "choice".'
msg = msg.format(option)
raise exceptions.ConfigurationError(msg)
if definition["type"] == "multiple_choice":
kwargs["type"] = VALIDATORS["csv"]
kwargs["choices"] = definition["choices"]
else:
msg = 'Unsupported type "{0}"'.format(definition["type"])
raise exception.ConfigurationError(msg)
if definition.get("hide"):
kwargs["help"] = argparse.SUPPRESS
group = definition.get("group", "MASTER").upper()
return group, args, kwargs
def parse(self, to_parse, config):
"""Parse the command line arguments into the given config object.
:param to_parse: The command line arguments to parse.
:type to_parse: list(str)
:param config: The config object to parse the command line into.
:type config: Configuration
"""
self._parser.parse_args(to_parse, config)
def preprocess(self, argv, *options):
"""Do some guess work to get a value for the specified option.
:param argv: The command line arguments to parse.
:type argv: list(str)
:param options: The names of the options to look for.
:type options: str
:returns: A config with the processed options.
:rtype: Configuration
"""
config = Configuration()
config.add_options(self._option_definitions.items())
args = self._parser.parse_known_args(argv)[0]
for option in options:
option = option.replace("-", "_")
config.set_option(option, getattr(args, option, None))
return config
def add_help_section(self, title, description, level=0):
"""Add an extra help section to the help message.
:param title: The title of the section.
This is included as part of the help message.
:type title: str
:param description: The description of the help section.
:type description: str
:param level: The minimum level of help needed to include this
in the help message.
:type level: int
"""
self._parser.add_argument_group(title, description, level=level)
class FileParser(ConfigParser, metaclass=abc.ABCMeta):
@abc.abstractmethod
def parse(self, to_parse, config):
pass
class IniFileParser(FileParser):
"""Parses a config files into config objects."""
def __init__(self):
super(IniFileParser, self).__init__()
self._parser = configparser.ConfigParser(
inline_comment_prefixes=("#", ";"), default_section="MASTER"
)
def add_option_definitions(self, option_definitions):
self._option_definitions.update(option_definitions)
for option, definition in option_definitions:
group, default = self._convert_definition(option, definition)
if group != self._parser.default_section:
try:
self._parser.add_section(group)
except configparser.DuplicateSectionError:
pass
else:
self._option_groups.add(group)
if default is not None:
self._parser["MASTER"].update(default)
@staticmethod
def _convert_definition(option, definition):
"""Convert an option definition to a set of arguments for the parser.
:param option: The name of the option.
:type option: str
:param definition: The argument definition to convert.
:type definition: dict
:returns: The converted definition.
:rtype: tuple(str, dict)
"""
default = None
if definition.get("default"):
unvalidator = UNVALIDATORS.get(definition.get("type"), str)
default_value = unvalidator(definition["default"])
default = {option: default_value}
group = definition.get("group", "MASTER").upper()
return group, default
def apply_to_configuration(self, configuration):
for section in self._parser.sections():
# Normalise the section titles
if not section.isupper():
new_section = section.upper()
for option, value in self._parser.items(section):
self._parser.set(new_section, option, value)
self._parser.remove_section(section)
section = section.upper()
for option, value in self._parser.items(section):
definition = self._option_definitions.get(option, {})
if isinstance(value, str):
type_ = definition.get("type")
validator = VALIDATORS.get(type_, lambda x: x)
value = validator(value)
configuration.set_option(option, value)
def parse(self, to_parse, config):
self._parser.read(to_parse)
self.apply_to_configuration(config)
def preprocess(self, to_parse, *options):
"""Do some guess work to get a value for the specified option.
:param to_parse: The path to the file to parse.
:type to_parse: str
:param options: The names of the options to look for.
:type options: str
:returns: A config with the processed options.
:rtype: Configuration
"""
config = Configuration()
config.add_options(self._option_definitions.items())
pre_config = Configuration()
self.parse(to_parse, pre_config)
for option in options:
setattr(config, option, getattr(pre_config, option, None))
return config
def write(self, stream=sys.stdout):
"""
Write out config to stream.
Includes option help and options with default values as
comments.
"""
# We can't reuse self._parser because it doesn't have the help
# comments. allow_no_value lets us add comments as keys.
write_parser = configparser.ConfigParser(
default_section="MASTER", allow_no_value=True
)
# This makes keys case sensitive, needed for preserving comment
# formatting.
write_parser.optionxform = str
configuration = Configuration()
self.apply_to_configuration(configuration)
config_set_args = self.make_set_args(self._option_definitions, configuration)
for args in config_set_args:
section = args[0]
if section != "MASTER" and not write_parser.has_section(section):
write_parser.add_section(section)
write_parser.set(*args)
write_parser.write(stream)
@staticmethod
def make_set_args(option_definitions, configuration):
"""Make args for configparser.ConfigParser.set."""
for option, definition in option_definitions.items():
section = definition.get("group", "MASTER").upper()
help_lines = [
"# {}".format(line)
for line in textwrap.wrap(definition.get("help", ""))
]
for help_line in help_lines:
# Calling set with only two args does not leave an =
# symbol at the end of the line.
yield (section, help_line)
default = definition.get("default")
option_type = definition.get("type", "string")
value = getattr(configuration, option)
if option_type == "csv":
str_value = ",\n".join(value)
else:
str_value = UNVALIDATORS[option_type](value)
if value == default:
key = "# {}".format(option)
str_value = "\n# ".join(str_value.split("\n"))
else:
key = option
yield (section, key, str_value)
class LongHelpFormatter(argparse.HelpFormatter):
output_level = None
def add_argument(self, action):
if action.level <= self.output_level:
super(LongHelpFormatter, self).add_argument(action)
def add_usage(self, usage, actions, groups, prefix=None):
actions = [action for action in actions if action.level <= self.output_level]
super(LongHelpFormatter, self).add_usage(usage, actions, groups, prefix)
class LongHelpAction(argparse.Action):
def __init__(
self,
option_strings,
dest=argparse.SUPPRESS,
default=argparse.SUPPRESS,
help=None,
):
super(LongHelpAction, self).__init__(
option_strings=option_strings,
dest=dest,
default=default,
nargs=0,
help=help,
)
self.level = 0
@staticmethod
def _parse_option_string(option_string):
level = 0
if option_string:
level = option_string.count("l-") or option_string.count("long-")
return level
@staticmethod
def build_add_args(level, prefix_chars="-"):
default_prefix = "-" if "-" in prefix_chars else prefix_chars[0]
return (
default_prefix + "-".join(["l"] * level) + "-h",
default_prefix * 2 + "-".join(["long"] * level) + "-help",
)
def __call__(self, parser, namespace, values, option_string=None):
level = self._parse_option_string(option_string)
parser.print_help(level=level)
parser.exit()
class LongHelpArgumentGroup(argparse._ArgumentGroup):
def __init__(self, *args, level=0, **kwargs):
super(LongHelpArgumentGroup, self).__init__(*args, **kwargs)
self.level = level
def add_argument(self, *args, **kwargs):
"""See :func:`argparse.ArgumentParser.add_argument`.
Patches in the level to each created action instance.
:returns: The created action.
:rtype: argparse.Action
"""
level = kwargs.pop("level", 0)
action = super(LongHelpArgumentGroup, self).add_argument(*args, **kwargs)
action.level = level
return action
class LongHelpArgumentParser(argparse.ArgumentParser):
def __init__(self, formatter_class=LongHelpFormatter, **kwargs):
self._max_level = 0
super(LongHelpArgumentParser, self).__init__(
formatter_class=formatter_class, **kwargs
)
# Stop ArgumentParser __init__ adding the wrong help formatter
def register(self, registry_name, value, object):
if registry_name == "action" and value == "help":
object = LongHelpAction
super(LongHelpArgumentParser, self).register(registry_name, value, object)
def _add_help_levels(self):
level = max(action.level for action in self._actions)
if level > self._max_level and self.add_help:
for new_level in range(self._max_level + 1, level + 1):
action = super(LongHelpArgumentParser, self).add_argument(
*LongHelpAction.build_add_args(new_level, self.prefix_chars),
action="help",
default=argparse.SUPPRESS,
help=(
"show this {0} verbose help message and exit".format(
" ".join(["really"] * (new_level - 1))
)
)
)
action.level = 0
self._max_level = level
def parse_known_args(self, *args, **kwargs):
self._add_help_levels()
return super(LongHelpArgumentParser, self).parse_known_args(*args, **kwargs)
def add_argument(self, *args, **kwargs):
"""See :func:`argparse.ArgumentParser.add_argument`.
Patches in the level to each created action instance.
:returns: The created action.
:rtype: argparse.Action
"""
level = kwargs.pop("level", 0)
action = super(LongHelpArgumentParser, self).add_argument(*args, **kwargs)
action.level = level
return action
def add_argument_group(self, *args, **kwargs):
group = LongHelpArgumentGroup(self, *args, **kwargs)
self._action_groups.append(group)
return group
# These methods use yucky way of passing the level to the formatter class
# without having to rely on argparse implementation details.
def format_usage(self, level=0):
if hasattr(self.formatter_class, "output_level"):
if self.formatter_class.output_level is None:
self.formatter_class.output_level = level
return super(LongHelpArgumentParser, self).format_usage()
def format_help(self, level=0):
if hasattr(self.formatter_class, "output_level"):
if self.formatter_class.output_level is None:
self.formatter_class.output_level = level
else:
level = self.formatter_class.output_level
# Unfortunately there's no way of publicly accessing the groups or
# an easy way of overriding format_help without using protected methods.
old_action_groups = self._action_groups
try:
self._action_groups = [
group for group in self._action_groups if group.level <= level
]
result = super(LongHelpArgumentParser, self).format_help()
finally:
self._action_groups = old_action_groups
return result
def print_usage(self, file=None, level=0):
if hasattr(self.formatter_class, "output_level"):
if self.formatter_class.output_level is None:
self.formatter_class.output_level = level
super(LongHelpArgumentParser, self).print_usage(file)
def print_help(self, file=None, level=0):
if hasattr(self.formatter_class, "output_level"):
if self.formatter_class.output_level is None:
self.formatter_class.output_level = level
super(LongHelpArgumentParser, self).print_help(file)