summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pylint/checkers/base_checker.py19
-rw-r--r--pylint/checkers/logging.py7
-rw-r--r--pylint/config/__init__.py4
-rw-r--r--pylint/config/argument.py76
-rw-r--r--pylint/config/arguments_manager.py107
-rw-r--r--pylint/config/config_initialization.py20
-rw-r--r--pylint/config/exceptions.py13
-rw-r--r--pylint/config/utils.py34
-rw-r--r--pylint/lint/pylinter.py9
-rw-r--r--pylintrc2
-rw-r--r--tests/config/data/logging_format_interpolation_style.py13
-rw-r--r--tests/config/data/logging_format_interpolation_style.rc2
-rw-r--r--tests/config/test_argparse_config.py56
-rw-r--r--tests/test_regr.py1
14 files changed, 353 insertions, 10 deletions
diff --git a/pylint/checkers/base_checker.py b/pylint/checkers/base_checker.py
index 1cdc014fc..21c63364a 100644
--- a/pylint/checkers/base_checker.py
+++ b/pylint/checkers/base_checker.py
@@ -3,18 +3,25 @@
# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
import functools
+import sys
from inspect import cleandoc
from typing import Any, Optional
from astroid import nodes
from pylint.config import OptionsProviderMixIn
+from pylint.config.exceptions import MissingArgumentManager
from pylint.constants import _MSG_ORDER, WarningScope
from pylint.exceptions import InvalidMessageError
from pylint.interfaces import Confidence, IRawChecker, ITokenChecker, implements
from pylint.message.message_definition import MessageDefinition
from pylint.utils import get_rst_section, get_rst_title
+if sys.version_info >= (3, 8):
+ from typing import Literal
+else:
+ from typing_extensions import Literal
+
@functools.total_ordering
class BaseChecker(OptionsProviderMixIn):
@@ -32,16 +39,26 @@ class BaseChecker(OptionsProviderMixIn):
# mark this checker as enabled or not.
enabled: bool = True
- def __init__(self, linter=None):
+ def __init__(
+ self, linter=None, *, future_option_parsing: Literal[None, True] = None
+ ):
"""Checker instances should have the linter as argument.
:param ILinter linter: is an object implementing ILinter.
+ :raises MissingArgumentManager: If no linter object is passed.
"""
if self.name is not None:
self.name = self.name.lower()
super().__init__()
self.linter = linter
+ if future_option_parsing:
+ # We need a PyLinter object that subclasses _ArgumentsManager to register options
+ if not self.linter:
+ raise MissingArgumentManager
+
+ self.linter._register_options_provider(self)
+
def __gt__(self, other):
"""Permit to sort a list of Checker by name."""
return f"{self.name}{self.msgs}".__gt__(f"{other.name}{other.msgs}")
diff --git a/pylint/checkers/logging.py b/pylint/checkers/logging.py
index 743f40db8..d9d73d3c1 100644
--- a/pylint/checkers/logging.py
+++ b/pylint/checkers/logging.py
@@ -144,15 +144,18 @@ class LoggingChecker(checkers.BaseChecker):
),
)
+ def __init__(self, linter: "PyLinter") -> None:
+ super().__init__(linter=linter, future_option_parsing=True)
+
def visit_module(self, _: nodes.Module) -> None:
"""Clears any state left in this checker from last module checked."""
# The code being checked can just as easily "import logging as foo",
# so it is necessary to process the imports and store in this field
# what name the logging module is actually given.
self._logging_names: Set[str] = set()
- logging_mods = self.config.logging_modules
+ logging_mods = self.linter.namespace.logging_modules
- self._format_style = self.config.logging_format_style
+ self._format_style = self.linter.namespace.logging_format_style
self._logging_modules = set(logging_mods)
self._from_imports = {}
diff --git a/pylint/config/__init__.py b/pylint/config/__init__.py
index 16df5fd89..a605fd9d3 100644
--- a/pylint/config/__init__.py
+++ b/pylint/config/__init__.py
@@ -8,6 +8,8 @@ import pickle
import sys
from datetime import datetime
+from pylint.config.argument import _Argument
+from pylint.config.arguments_manager import _ArgumentsManager
from pylint.config.configuration_mixin import ConfigurationMixIn
from pylint.config.find_default_config_files import (
find_default_config_files,
@@ -22,6 +24,8 @@ from pylint.constants import DEFAULT_PYLINT_HOME, OLD_DEFAULT_PYLINT_HOME
from pylint.utils import LinterStats
__all__ = [
+ "_Argument",
+ "_ArgumentsManager",
"ConfigurationMixIn",
"find_default_config_files",
"_ManHelpFormatter",
diff --git a/pylint/config/argument.py b/pylint/config/argument.py
new file mode 100644
index 000000000..bf35f1a85
--- /dev/null
+++ b/pylint/config/argument.py
@@ -0,0 +1,76 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
+
+"""Definition of an Argument class and validators for various argument types.
+
+An Argument instance represents a pylint option to be handled by an argparse.ArgumentParser
+"""
+
+
+from typing import Any, Callable, Dict, List, Optional, Union
+
+from pylint import utils as pylint_utils
+
+_ArgumentTypes = Union[str, List[str]]
+"""List of possible argument types."""
+
+
+def _csv_validator(value: Union[str, List[str]]) -> List[str]:
+ """Validates a comma separated string."""
+ return pylint_utils._check_csv(value)
+
+
+_ASSIGNMENT_VALIDATORS: Dict[str, Callable[[Any], _ArgumentTypes]] = {
+ "choice": str,
+ "csv": _csv_validator,
+}
+"""Validators for all assignment types."""
+
+
+class _Argument:
+ """Class representing an argument to be passed by an argparse.ArgumentsParser.
+
+ This is based on the parameters passed to argparse.ArgumentsParser.add_message.
+ See:
+ https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument
+ """
+
+ def __init__(
+ self,
+ flags: List[str],
+ action: str,
+ default: _ArgumentTypes,
+ arg_type: str,
+ choices: Optional[List[str]],
+ arg_help: str,
+ metavar: str,
+ ) -> None:
+ self.flags = flags
+ """The name of the argument."""
+
+ self.action = action
+ """The action to perform with the argument."""
+
+ self.type = _ASSIGNMENT_VALIDATORS[arg_type]
+ """A validator function that returns and checks the type of the argument."""
+
+ self.default = self.type(default)
+ """The default value of the argument."""
+
+ self.choices = choices
+ """A list of possible choices for the argument.
+
+ None if there are no restrictions.
+ """
+
+ # argparse uses % formatting on help strings, so a % needs to be escaped
+ self.help = arg_help.replace("%", "%%")
+ """The description of the argument."""
+
+ self.metavar = metavar
+ """The metavar of the argument.
+
+ See:
+ https://docs.python.org/3/library/argparse.html#metavar
+ """
diff --git a/pylint/config/arguments_manager.py b/pylint/config/arguments_manager.py
new file mode 100644
index 000000000..300ded617
--- /dev/null
+++ b/pylint/config/arguments_manager.py
@@ -0,0 +1,107 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
+
+"""Arguments manager class used to handle command-line arguments and options."""
+
+import argparse
+import configparser
+from typing import TYPE_CHECKING, Dict, List
+
+from pylint.config.argument import _Argument
+from pylint.config.exceptions import UnrecognizedArgumentAction
+from pylint.config.utils import _convert_option_to_argument
+
+if TYPE_CHECKING:
+ from pylint import checkers
+
+
+class _ArgumentsManager:
+ """Arguments manager class used to handle command-line arguments and options."""
+
+ def __init__(self) -> None:
+ self.namespace = argparse.Namespace()
+ """Namespace for all options."""
+
+ self._arg_parser = argparse.ArgumentParser(prog="pylint")
+ """The command line argument parser."""
+
+ self._argument_groups_dict: Dict[str, argparse._ArgumentGroup] = {}
+ """Dictionary of all the argument groups."""
+
+ def _register_options_provider(self, provider: "checkers.BaseChecker") -> None:
+ """Register an options provider and load its defaults."""
+ # pylint: disable-next=fixme
+ # TODO: Do something own_group parameter (see OptionsManagerMixIn.register_options_provider)
+ for opt, optdict in provider.options:
+ argument = _convert_option_to_argument(opt, optdict)
+ self._add_arguments_to_parser(provider.name, argument)
+
+ # pylint: disable-next=fixme
+ # TODO: Do something with option provider priorities (see OptionsManagerMixIn.register_options_provider)
+ # pylint: disable-next=fixme
+ # TODO: Do something with option groups within optdicts (see OptionsManagerMixIn.register_options_provider)
+
+ # pylint: disable-next=fixme
+ # TODO: Investigate performance impact of loading default arguments on every call
+ self._load_default_argument_values()
+
+ def _add_arguments_to_parser(self, section: str, argument: _Argument) -> None:
+ """Iterates over all argument sections and add them to the parser object."""
+ try:
+ section_group = self._argument_groups_dict[section]
+ except KeyError:
+ section_group = self._arg_parser.add_argument_group(title=section)
+ self._argument_groups_dict[section] = section_group
+ self._add_parser_option(section_group, argument)
+
+ @staticmethod
+ def _add_parser_option(
+ section_group: argparse._ArgumentGroup, argument: _Argument
+ ) -> None:
+ """Add an argument."""
+ if argument.action == "store":
+ section_group.add_argument(
+ *argument.flags,
+ action=argument.action,
+ default=argument.default,
+ type=argument.type, # type: ignore[arg-type] # incorrect typing in typeshed
+ help=argument.help,
+ metavar=argument.metavar,
+ choices=argument.choices,
+ )
+ else:
+ raise UnrecognizedArgumentAction
+
+ def _load_default_argument_values(self) -> None:
+ """Loads the default values of all registered options."""
+ self.namespace = self._arg_parser.parse_args([], self.namespace)
+
+ def _parse_configuration_file(
+ self, config_parser: configparser.ConfigParser
+ ) -> None:
+ """Parse the arguments found in a configuration file into the namespace."""
+ arguments = []
+ for section in config_parser.sections():
+ for opt, value in config_parser.items(section):
+ arguments.extend([f"--{opt}", value])
+ # pylint: disable-next=fixme
+ # TODO: This should parse_args instead of parse_known_args
+ self.namespace = self._arg_parser.parse_known_args(arguments, self.namespace)[0]
+
+ def _parse_command_line_configuration(self, arguments: List[str]) -> None:
+ """Parse the arguments found on the command line into the namespace."""
+ # pylint: disable-next=fixme
+ # TODO: This should parse_args instead of parse_known_args
+ self.namespace = self._arg_parser.parse_known_args(arguments, self.namespace)[0]
+
+ # pylint: disable-next=fixme
+ # TODO: This should return a list of arguments with the option arguments removed
+ # just as PyLinter.load_command_line_configuration does
+
+ def _parse_plugin_configuration(self) -> None:
+ # pylint: disable-next=fixme
+ # TODO: This is not currently implemented.
+ # Perhaps we should also change the name to distuingish it better?
+ # See PyLinter.load_plugin_configuration
+ pass
diff --git a/pylint/config/config_initialization.py b/pylint/config/config_initialization.py
index 6be0ab8b9..f431a3c8f 100644
--- a/pylint/config/config_initialization.py
+++ b/pylint/config/config_initialization.py
@@ -55,7 +55,7 @@ def _config_initialization(
# Load command line arguments
try:
- args_list = linter.load_command_line_configuration(args_list)
+ parsed_args_list = linter.load_command_line_configuration(args_list)
except SystemExit as exc:
if exc.code == 2: # bad options
exc.code = 32
@@ -63,7 +63,7 @@ def _config_initialization(
# args_list should now only be a list of files/directories to lint. All options have
# been removed from the list
- if not args_list:
+ if not parsed_args_list:
print(linter.help())
sys.exit(32)
@@ -71,7 +71,21 @@ def _config_initialization(
# load plugin specific configuration.
linter.load_plugin_configuration()
+ # pylint: disable-next=fixme
+ # TODO: Start of the implemenation of argument parsing with argparse
+ # When finished this should replace the implementation based on optparse
+
+ # First we parse any options from a configuration file
+ linter._parse_configuration_file(config_parser)
+
+ # Second we parse any options from the command line, so they can override
+ # the configuration file
+ linter._parse_command_line_configuration(args_list)
+
+ # Lastly we parse any options from plugins
+ linter._parse_plugin_configuration()
+
# Now that plugins are loaded, get list of all fail_on messages, and enable them
linter.enable_fail_on_messages()
- return args_list
+ return parsed_args_list
diff --git a/pylint/config/exceptions.py b/pylint/config/exceptions.py
new file mode 100644
index 000000000..4c1c4a678
--- /dev/null
+++ b/pylint/config/exceptions.py
@@ -0,0 +1,13 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
+
+
+class UnrecognizedArgumentAction(Exception):
+ """Raised if an ArgumentManager instance tries to add an argument for which the action
+ is not recognized.
+ """
+
+
+class MissingArgumentManager(Exception):
+ """Raised if an ArgumentManager instance to register options to is missing."""
diff --git a/pylint/config/utils.py b/pylint/config/utils.py
new file mode 100644
index 000000000..352a130e6
--- /dev/null
+++ b/pylint/config/utils.py
@@ -0,0 +1,34 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
+
+"""Utils for arguments/options parsing and handling."""
+
+
+from typing import Any, Dict
+
+from pylint.config.argument import _Argument
+
+IMPLEMENTED_OPTDICT_KEYS = {"action", "default", "type", "choices", "help", "metavar"}
+"""This is used to track our progress on accepting all optdict keys."""
+
+
+def _convert_option_to_argument(opt: str, optdict: Dict[str, Any]) -> _Argument:
+ """Convert an optdict to an Argument class instance."""
+ # See if the optdict contains any keys we don't yet implement
+ # pylint: disable-next=fixme
+ # TODO: This should be removed once the migration to argparse is finished
+ for key, value in optdict.items():
+ if key not in IMPLEMENTED_OPTDICT_KEYS:
+ print("Unhandled key found in Argument creation:", key) # pragma: no cover
+ print("It's value is:", value) # pragma: no cover
+
+ return _Argument(
+ flags=[f"--{opt}"],
+ action=optdict.get("action", "store"),
+ default=optdict["default"],
+ arg_type=optdict["type"],
+ choices=optdict.get("choices", None),
+ arg_help=optdict["help"],
+ metavar=optdict["metavar"],
+ )
diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py
index 0bf8961c1..d54d7adfc 100644
--- a/pylint/lint/pylinter.py
+++ b/pylint/lint/pylinter.py
@@ -193,6 +193,7 @@ class PyLinter(
config.OptionsManagerMixIn,
reporters.ReportsHandlerMixIn,
checkers.BaseTokenChecker,
+ config._ArgumentsManager,
):
"""Lint Python modules using external checkers.
@@ -551,9 +552,11 @@ class PyLinter(
option_groups: Tuple[Tuple[str, str], ...] = (),
pylintrc: Optional[str] = None,
) -> None:
- """Some stuff has to be done before ancestors initialization...
- messages store / checkers / reporter / astroid manager
- """
+ config._ArgumentsManager.__init__(self)
+
+ # Some stuff has to be done before initialization of other ancestors...
+ # messages store / checkers / reporter / astroid manager
+
# Attributes for reporters
self.reporter: Union[reporters.BaseReporter, reporters.MultiReporter]
if reporter:
diff --git a/pylintrc b/pylintrc
index 1c28d5484..7dd03b686 100644
--- a/pylintrc
+++ b/pylintrc
@@ -393,7 +393,7 @@ ignored-classes=SQLObject, optparse.Values, thread._local, _thread._local
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
-generated-members=REQUEST,acl_users,aq_parent
+generated-members=REQUEST,acl_users,aq_parent,argparse.Namespace
# List of decorators that create context managers from functions, such as
# contextlib.contextmanager.
diff --git a/tests/config/data/logging_format_interpolation_style.py b/tests/config/data/logging_format_interpolation_style.py
new file mode 100644
index 000000000..04b357abd
--- /dev/null
+++ b/tests/config/data/logging_format_interpolation_style.py
@@ -0,0 +1,13 @@
+"""This should raise various logging-too-many-args messages if
+logging-format-style is not 'new'.
+"""
+
+import logging
+
+logging.error("constant string")
+logging.error("{}")
+logging.error("{}", 1)
+logging.error("{0}", 1)
+logging.error("{named}", {"named": 1})
+logging.error("{} {named}", 1, {"named": 1})
+logging.error("{0} {named}", 1, {"named": 1})
diff --git a/tests/config/data/logging_format_interpolation_style.rc b/tests/config/data/logging_format_interpolation_style.rc
new file mode 100644
index 000000000..745bbd9cf
--- /dev/null
+++ b/tests/config/data/logging_format_interpolation_style.rc
@@ -0,0 +1,2 @@
+[LOGGING]
+logging-format-style=new
diff --git a/tests/config/test_argparse_config.py b/tests/config/test_argparse_config.py
new file mode 100644
index 000000000..cc399d995
--- /dev/null
+++ b/tests/config/test_argparse_config.py
@@ -0,0 +1,56 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
+
+"""Test for the (new) implementation of option parsing with argparse"""
+
+from os.path import abspath, dirname, join
+
+import pytest
+
+from pylint.lint import Run
+
+HERE = abspath(dirname(__file__))
+REGRTEST_DATA_DIR = join(HERE, "..", "regrtest_data")
+EMPTY_MODULE = join(REGRTEST_DATA_DIR, "empty.py")
+LOGGING_TEST = join(HERE, "data", "logging_format_interpolation_style.py")
+
+
+class TestArgumentsManager:
+ """Tests for the ArgumentsManager class"""
+
+ base_run = Run([EMPTY_MODULE], exit=False)
+
+ def test_namespace_creation(self) -> None:
+ """Test that the linter object has a namespace attribute and that it is not empty"""
+
+ assert self.base_run.linter.namespace
+ assert self.base_run.linter.namespace._get_kwargs()
+
+
+class TestArgparseOptionsProviderMixin:
+ """Tests for the argparse implementation of OptionsProviderMixIn.
+
+ The logger checker is used as an example checker for this implementation.
+ """
+
+ @staticmethod
+ def test_logger_without_options() -> None:
+ """Check that we raise messages when we do not supply any options."""
+ with pytest.raises(SystemExit) as ex:
+ Run([LOGGING_TEST])
+ assert ex.value.code == 2
+
+ @staticmethod
+ def test_logger_commandline() -> None:
+ """Check that we parse command-line options for the logging checker correctly."""
+ with pytest.raises(SystemExit) as ex:
+ Run([LOGGING_TEST, "--logging-format-style=new"])
+ assert ex.value.code == 0
+
+ @staticmethod
+ def test_logger_rcfile() -> None:
+ """Check that we parse the rcfile for the logging checker correctly."""
+ with pytest.raises(SystemExit) as ex:
+ Run([LOGGING_TEST, f"--rcfile={LOGGING_TEST.replace('.py', '.rc')}"])
+ assert ex.value.code == 0
diff --git a/tests/test_regr.py b/tests/test_regr.py
index 76bcafd68..e3c14c8a0 100644
--- a/tests/test_regr.py
+++ b/tests/test_regr.py
@@ -128,6 +128,7 @@ def test_pylint_config_attr() -> None:
"BaseTokenChecker",
"BaseChecker",
"OptionsProviderMixIn",
+ "_ArgumentsManager",
]
assert [c.name for c in pylinter.ancestors()] == expect
assert list(astroid.Instance(pylinter).getattr("config"))