Skip to content

Commit

Permalink
Move preprocessing of arguments into config directory
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielNoord committed Apr 4, 2022
1 parent 3f72dbc commit ed2faa1
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 133 deletions.
4 changes: 4 additions & 0 deletions pylint/config/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ class UnrecognizedArgumentAction(Exception):

class MissingArgumentManager(Exception):
"""Raised if an ArgumentManager instance to register options to is missing."""


class ArgumentPreprocessingError(Exception):
"""Raised if an error occurs during argument preprocessing."""
101 changes: 100 additions & 1 deletion pylint/config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@


import warnings
from typing import Any, Dict, Union
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union

from pylint import extensions, utils
from pylint.config.argument import _CallableArgument, _StoreArgument, _StoreTrueArgument
from pylint.config.callback_actions import _CallbackAction
from pylint.config.exceptions import ArgumentPreprocessingError

if TYPE_CHECKING:
from pylint.lint.run import Run


def _convert_option_to_argument(
Expand Down Expand Up @@ -75,3 +81,96 @@ def _parse_rich_type_value(value: Any) -> str:
if isinstance(value, (list, tuple)):
return ",".join(value)
return str(value)


# pylint: disable-next=unused-argument
def _init_hook(run: "Run", value: Optional[str]) -> None:
"""Execute arbitrary code from the init_hook.
This can be used to set the 'sys.path' for example.
"""
assert value is not None
exec(value) # pylint: disable=exec-used


def _set_rcfile(run: "Run", value: Optional[str]) -> None:
"""Set the rcfile."""
assert value is not None
run._rcfile = value


def _set_output(run: "Run", value: Optional[str]) -> None:
"""Set the output."""
assert value is not None
run._output = value


def _add_plugins(run: "Run", value: Optional[str]) -> None:
"""Add plugins to the list of loadable plugins."""
assert value is not None
run._plugins.extend(utils._splitstrip(value))


def _set_verbose_mode(run: "Run", value: Optional[str]) -> None:
assert value is None
run.verbose = True


def _enable_all_extensions(run: "Run", value: Optional[str]) -> None:
"""Enable all extensions."""
assert value is None
for filename in Path(extensions.__file__).parent.iterdir():
if filename.suffix == ".py" and not filename.stem.startswith("_"):
extension_name = f"pylint.extensions.{filename.stem}"
if extension_name not in run._plugins:
run._plugins.append(extension_name)


PREPROCESSABLE_OPTIONS: Dict[
str, Tuple[bool, Callable[["Run", Optional[str]], None]]
] = { # pylint: disable=consider-using-namedtuple-or-dataclass
"--init-hook": (True, _init_hook),
"--rcfile": (True, _set_rcfile),
"--output": (True, _set_output),
"--load-plugins": (True, _add_plugins),
"--verbose": (False, _set_verbose_mode),
"--enable-all-extensions": (False, _enable_all_extensions),
}


def _preprocess_options(run: "Run", args: List[str]) -> List[str]:
"""Preprocess options before full config parsing has started."""
processed_args: List[str] = []

i = 0
while i < len(args):
argument = args[i]
if not argument.startswith("--"):
processed_args.append(argument)
i += 1
continue

try:
option, value = argument.split("=", 1)
except ValueError:
option, value = argument, None

if option not in PREPROCESSABLE_OPTIONS:
processed_args.append(argument)
i += 1
continue

takearg, cb = PREPROCESSABLE_OPTIONS[option]

if takearg and value is None:
i += 1
if i >= len(args) or args[i].startswith("-"):
raise ArgumentPreprocessingError(f"Option {option} expects a value")
value = args[i]
elif not takearg and value is not None:
raise ArgumentPreprocessingError(f"Option {option} doesn't expects a value")

cb(run, value)
i += 1

return processed_args
9 changes: 2 additions & 7 deletions pylint/lint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"""
import sys

from pylint.config.exceptions import ArgumentPreprocessingError
from pylint.lint.parallel import check_parallel
from pylint.lint.pylinter import PyLinter
from pylint.lint.report_functions import (
Expand All @@ -24,12 +25,7 @@
report_total_messages_stats,
)
from pylint.lint.run import Run
from pylint.lint.utils import (
ArgumentPreprocessingError,
_patch_sys_path,
fix_import_path,
preprocess_options,
)
from pylint.lint.utils import _patch_sys_path, fix_import_path

__all__ = [
"check_parallel",
Expand All @@ -41,7 +37,6 @@
"ArgumentPreprocessingError",
"_patch_sys_path",
"fix_import_path",
"preprocess_options",
]

if __name__ == "__main__":
Expand Down
62 changes: 10 additions & 52 deletions pylint/lint/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import os
import sys
import warnings
from typing import Optional
from typing import List, Optional

from pylint import config, extensions
from pylint import config
from pylint.config.callback_actions import (
_DoNothingAction,
_ErrorsOnlyModeAction,
Expand All @@ -22,15 +22,10 @@
_MessageHelpAction,
)
from pylint.config.config_initialization import _config_initialization
from pylint.config.exceptions import ArgumentPreprocessingError
from pylint.config.utils import _preprocess_options
from pylint.constants import full_version
from pylint.lint.pylinter import PyLinter
from pylint.lint.utils import ArgumentPreprocessingError, preprocess_options
from pylint.utils import utils

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

try:
import multiprocessing
Expand All @@ -50,11 +45,6 @@ def _cpu_count() -> int:
return 1


def cb_init_hook(optname, value):
"""Execute arbitrary code to set 'sys.path' for instance."""
exec(value) # pylint: disable=exec-used


UNUSED_PARAM_SENTINEL = object()


Expand Down Expand Up @@ -89,22 +79,13 @@ def __init__(
sys.exit(0)

self._rcfile: Optional[str] = None
self._output = None
self._plugins = []
self.verbose = None
self._output: Optional[str] = None
self._plugins: List[str] = []
self.verbose: bool = False

# Preprocess certain options and remove them from args list
try:
preprocess_options(
args,
{
# option: (callback, takearg)
"init-hook": (cb_init_hook, True),
"rcfile": (self.cb_set_rcfile, True),
"load-plugins": (self.cb_add_plugins, True),
"enable-all-extensions": (self.cb_enable_all_extensions, False),
"verbose": (self.cb_verbose_mode, False),
"output": (self.cb_set_output, True),
},
)
args = _preprocess_options(self, args)
except ArgumentPreprocessingError as ex:
print(ex, file=sys.stderr)
sys.exit(32)
Expand Down Expand Up @@ -342,26 +323,3 @@ def __init__(
sys.exit(self.linter.msg_status or 1)
else:
sys.exit(self.linter.msg_status)

def cb_set_rcfile(self, name: Literal["rcfile"], value: str) -> None:
"""Callback for option preprocessing (i.e. before option parsing)."""
self._rcfile = value

def cb_set_output(self, name, value):
"""Callback for option preprocessing (i.e. before option parsing)."""
self._output = value

def cb_add_plugins(self, name, value):
"""Callback for option preprocessing (i.e. before option parsing)."""
self._plugins.extend(utils._splitstrip(value))

def cb_verbose_mode(self, *args, **kwargs):
self.verbose = True

def cb_enable_all_extensions(self, option_name: str, value: None) -> None:
"""Callback to load and enable all available extensions."""
for filename in os.listdir(os.path.dirname(extensions.__file__)):
if filename.endswith(".py") and not filename.startswith("_"):
extension_name = f"pylint.extensions.{filename[:-3]}"
if extension_name not in self._plugins:
self._plugins.append(extension_name)
39 changes: 0 additions & 39 deletions pylint/lint/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@
from pylint.lint.expand_modules import get_python_path


class ArgumentPreprocessingError(Exception):
"""Raised if an error occurs during argument preprocessing."""


def prepare_crash_report(ex: Exception, filepath: str, crash_file_path: str) -> Path:
issue_template_path = (
Path(PYLINT_HOME) / datetime.now().strftime(str(crash_file_path))
Expand Down Expand Up @@ -72,41 +68,6 @@ def get_fatal_error_message(filepath: str, issue_template_path: Path) -> str:
)


def preprocess_options(args, search_for):
"""Look for some options (keys of <search_for>) which have to be processed
before others
values of <search_for> are callback functions to call when the option is
found
"""
i = 0
while i < len(args):
arg = args[i]
if not arg.startswith("--"):
i += 1
else:
try:
option, val = arg[2:].split("=", 1)
except ValueError:
option, val = arg[2:], None
try:
cb, takearg = search_for[option]
except KeyError:
i += 1
else:
del args[i]
if takearg and val is None:
if i >= len(args) or args[i].startswith("-"):
msg = f"Option {option} expects a value"
raise ArgumentPreprocessingError(msg)
val = args[i]
del args[i]
elif not takearg and val is not None:
msg = f"Option {option} doesn't expects a value"
raise ArgumentPreprocessingError(msg)
cb(option, val)


def _patch_sys_path(args):
original = list(sys.path)
changes = []
Expand Down
38 changes: 4 additions & 34 deletions tests/lint/unittest_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from os import chdir, getcwd
from os.path import abspath, dirname, join, sep
from shutil import rmtree
from typing import Iterable, Iterator, List, Optional, Tuple
from typing import Iterable, Iterator, List

import platformdirs
import pytest
Expand All @@ -29,7 +29,7 @@
OLD_DEFAULT_PYLINT_HOME,
)
from pylint.exceptions import InvalidMessageError
from pylint.lint import ArgumentPreprocessingError, PyLinter, Run, preprocess_options
from pylint.lint import PyLinter, Run
from pylint.message import Message
from pylint.reporters import text
from pylint.testutils import create_files
Expand Down Expand Up @@ -538,6 +538,8 @@ def test_init_hooks_called_before_load_plugins() -> None:
Run(["--load-plugins", "unexistant", "--init-hook", "raise RuntimeError"])
with pytest.raises(RuntimeError):
Run(["--init-hook", "raise RuntimeError", "--load-plugins", "unexistant"])
with pytest.raises(SystemExit):
Run(["--init-hook"])


def test_analyze_explicit_script(linter: PyLinter) -> None:
Expand Down Expand Up @@ -706,38 +708,6 @@ def test_pylintrc_parentdir_no_package() -> None:
assert config.find_pylintrc() == expected


class TestPreprocessOptions:
def _callback(self, name: str, value: Optional[str]) -> None:
self.args.append((name, value))

def test_value_equal(self) -> None:
self.args: List[Tuple[str, Optional[str]]] = []
preprocess_options(
["--foo", "--bar=baz", "--qu=ux"],
{"foo": (self._callback, False), "qu": (self._callback, True)},
)
assert [("foo", None), ("qu", "ux")] == self.args

def test_value_space(self) -> None:
self.args = []
preprocess_options(["--qu", "ux"], {"qu": (self._callback, True)})
assert [("qu", "ux")] == self.args

@staticmethod
def test_error_missing_expected_value() -> None:
with pytest.raises(ArgumentPreprocessingError):
preprocess_options(["--foo", "--bar", "--qu=ux"], {"bar": (None, True)})
with pytest.raises(ArgumentPreprocessingError):
preprocess_options(["--foo", "--bar"], {"bar": (None, True)})

@staticmethod
def test_error_unexpected_value() -> None:
with pytest.raises(ArgumentPreprocessingError):
preprocess_options(
["--foo", "--bar=spam", "--qu=ux"], {"bar": (None, False)}
)


class _CustomPyLinter(PyLinter):
@staticmethod
def should_analyze_file(modname: str, path: str, is_argument: bool = False) -> bool:
Expand Down
4 changes: 4 additions & 0 deletions tests/test_self.py
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,10 @@ def test_verbose() -> None:
run = Run(["--verbose"])
assert run.verbose

with pytest.raises(SystemExit):
run = Run(["--verbose=True"])
assert run.verbose

@staticmethod
def test_enable_all_extensions() -> None:
"""Test to see if --enable-all-extensions does indeed load all extensions."""
Expand Down

0 comments on commit ed2faa1

Please sign in to comment.