Skip to content

Commit

Permalink
Test for pickling
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielNoord committed Dec 22, 2021
1 parent c0fbbb7 commit c447d7b
Show file tree
Hide file tree
Showing 11 changed files with 229 additions and 4 deletions.
12 changes: 10 additions & 2 deletions pylint/checkers/base_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# 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
import functools
import sys
from inspect import cleandoc
from typing import Any, Optional

Expand All @@ -30,6 +31,11 @@
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):
Expand All @@ -47,14 +53,16 @@ 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."""
if self.name is not None:
self.name = self.name.lower()
super().__init__()
self.linter = linter
super().__init__(future=future_option_parsing)

def __gt__(self, other):
"""Permit to sort a list of Checker by name."""
Expand Down
3 changes: 3 additions & 0 deletions pylint/checkers/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ class LoggingChecker(checkers.BaseChecker):
),
)

def __init__(self, linter=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",
Expand Down
5 changes: 5 additions & 0 deletions pylint/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import sys
from datetime import datetime

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,
Expand All @@ -50,10 +51,14 @@
from pylint.config.option_manager_mixin import OptionsManagerMixIn
from pylint.config.option_parser import OptionParser
from pylint.config.options_provider_mixin import OptionsProviderMixIn, UnsupportedAction
from pylint.config.utils import _Argument, _validate_argument
from pylint.constants import DEFAULT_PYLINT_HOME, OLD_DEFAULT_PYLINT_HOME
from pylint.utils import LinterStats

__all__ = [
"_ArgumentsManager",
"_Argument",
"_validate_argument",
"ConfigurationMixIn",
"find_default_config_files",
"_ManHelpFormatter",
Expand Down
66 changes: 66 additions & 0 deletions pylint/config/argument.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 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

"""Definition of an Argument class and validators for various argument types.
An Argument instance represents a pylint option to be handled by an ArgumentsParser,
which should be a subclass or instance of argparse.ArgumentsParser.
"""


from typing import Any, Callable, Dict, List, Optional, Union

from pylint import utils as pylint_utils

ArgumentTypes = Union[str, List[str]]
"""List of types 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,
name: str,
default: ArgumentTypes,
arg_type: str,
arg_help: str,
metavar: str,
choices: Optional[List[str]] = None,
) -> None:
self.name = name
"""The name of 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 or []
"""A list of possible choices for the argument. The list is empty if there are no restrictions."""

self.help = arg_help
"""The description of the argument"""

self.metavar = metavar
"""The metavar of the argument.
See:
https://docs.python.org/3/library/argparse.html#metavar
"""
23 changes: 23 additions & 0 deletions pylint/config/arguments_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 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

"""Arguments manager class used to handle command-line arguments and options"""

import argparse


class _ArgumentsManager:
"""Arguments manager class used to handle command-line arguments and options"""

def __init__(self, namespace: argparse.Namespace) -> None:
"""We don't set the parser as an instance attribute as the ArgumentParser class
can't be pickled on Windows. This blocks running pylint in parallel jobs.
See: https://github.com/PyCQA/pylint/pull/5584#issuecomment-999190647"""
parser = argparse.ArgumentParser(prog="pylint")

self.namespace = namespace
self.help_message = self._get_help_message(parser)

@staticmethod
def _get_help_message(parser: argparse.ArgumentParser) -> str:
return parser.format_help()
13 changes: 12 additions & 1 deletion pylint/config/options_provider_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@


import optparse # pylint: disable=deprecated-module
import sys
from typing import Any, Dict, Tuple

from pylint.config import utils
from pylint.config.option import _validate

if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal


class UnsupportedAction(Exception):
"""raised by set_option when it doesn't know what to do for an action"""
Expand All @@ -21,7 +28,11 @@ class OptionsProviderMixIn:
options: Tuple[Tuple[str, Dict[str, Any]], ...] = ()
level = 0

def __init__(self):
def __init__(self, *, future: Literal[None, True] = None):
if future:
for opt, optdict in self.options:
argument = utils._convert_option_to_argument(opt, optdict)
self.linter.namespace_test[opt] = argument # type: ignore[attr-defined] # Always mixed in with a checker
self.config = optparse.Values()
self.load_defaults()

Expand Down
53 changes: 53 additions & 0 deletions pylint/config/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 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

"""Utils for arguments/options parsing and handling."""

from typing import Any, Dict

from pylint.config.argument import _Argument

ArgumentsDict = Dict[str, _Argument]

IMPLEMENTED_OPTDICT_KEYS = {"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
for key, value in optdict.items():
if key not in IMPLEMENTED_OPTDICT_KEYS:
print("Unhandled key found in Argument creation:", key)
print("It's value is:", value)
return _Argument(
name=opt,
default=optdict["default"],
arg_type=optdict["type"],
choices=optdict.get("choices", []),
arg_help=optdict["help"],
metavar=optdict["metavar"],
)


def _validate_argument(argument: _Argument) -> None:
"""Method to check if all instance attributes have the correct typed."""
# Check default value
assert argument.default
assert argument.default == argument.type(argument.default)

# Check type value
assert argument.type
assert callable(argument.type)

# Check choices value
assert isinstance(argument.choices, list)
assert all(isinstance(i, str) for i in argument.choices)

# Check help value
assert argument.help
assert isinstance(argument.help, str)

# Check metavar value
assert argument.metavar
assert isinstance(argument.metavar, str)
6 changes: 6 additions & 0 deletions pylint/lint/pylinter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 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

import argparse
import collections
import contextlib
import functools
Expand Down Expand Up @@ -192,6 +193,7 @@ class PyLinter(
config.OptionsManagerMixIn,
reporters.ReportsHandlerMixIn,
checkers.BaseTokenChecker,
config._ArgumentsManager,
):
"""lint Python modules using external checkers.
Expand Down Expand Up @@ -543,6 +545,9 @@ def __init__(
):
"""Some stuff has to be done before ancestors initialization...
messages store / checkers / reporter / astroid manager"""
self.namespace_test: Dict[str, config._Argument] = {}
"""Temporary dictionary for testing purposes"""

# Attributes for reporters
self.reporter: Union[reporters.BaseReporter, reporters.MultiReporter]
if reporter:
Expand Down Expand Up @@ -600,6 +605,7 @@ def __init__(
self._by_id_managed_msgs: List[ManagedMessage] = []

reporters.ReportsHandlerMixIn.__init__(self)
config._ArgumentsManager.__init__(self, argparse.Namespace())
super().__init__(
usage=__doc__,
config_file=pylintrc or next(config.find_default_config_files(), None),
Expand Down
49 changes: 49 additions & 0 deletions tests/config/test_argparse_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Test for the (new) implementation of option parsing with argparse"""

import argparse
from os.path import abspath, dirname, join

from pylint import config
from pylint.lint import PyLinter, Run

HERE = abspath(dirname(__file__))
REGRTEST_DATA_DIR = join(HERE, "..", "regrtest_data")


class TestArgumentsManager:
"""Tests for the ArgumentsManager class"""

@staticmethod
def test_initialize_arguments_manager_class(linter: PyLinter) -> None:
"""Tests that ArgumentsManager is correctly initialized on a PyLinter class"""
assert isinstance(linter, config._ArgumentsManager)
assert isinstance(linter.namespace, argparse.Namespace)

@staticmethod
def test_help_message(linter: PyLinter) -> None:
"""Test the help_message attribute of the ArgumentsManager class"""
assert isinstance(linter.help_message, str)
lines = linter.help_message.splitlines()
assert lines[0] == "usage: pylint [-h]"


class TestOptionsProviderMixin:
"""Tests for the argparse implementation of OptionsProviderMixin"""

empty_module = join(REGRTEST_DATA_DIR, "empty.py")

def test_logger_checker(self) -> None:
"""Tests that we create Argument instances for the settings of the logger module"""
run = Run([self.empty_module], exit=False)
assert run.linter.namespace_test
assert len(run.linter.namespace_test) == 2

assert "logging-modules" in run.linter.namespace_test
logging_modules = run.linter.namespace_test["logging-modules"]
assert isinstance(logging_modules, config._Argument)
config._validate_argument(logging_modules)

assert "logging-format-style" in run.linter.namespace_test
logging_format_style = run.linter.namespace_test["logging-format-style"]
assert isinstance(logging_format_style, config._Argument)
config._validate_argument(logging_format_style)
2 changes: 1 addition & 1 deletion tests/lint/unittest_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ class FakeConfig:
assert MSG_STATE_SCOPE_MODULE == linter._get_message_state_scope("W0101", 3)
linter.enable("W0102", scope="module", line=3)
assert MSG_STATE_SCOPE_MODULE == linter._get_message_state_scope("W0102", 3)
linter.config = FakeConfig()
linter.config = FakeConfig() # type: ignore[assignment] # This should be optparse.Values()
assert MSG_STATE_CONFIDENCE == linter._get_message_state_scope(
"this-is-bad", confidence=interfaces.INFERENCE
)
Expand Down
1 change: 1 addition & 0 deletions tests/test_regr.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,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"))
Expand Down

0 comments on commit c447d7b

Please sign in to comment.