summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniƫl van Noord <13665637+DanielNoord@users.noreply.github.com>2022-06-04 13:49:52 +0200
committerGitHub <noreply@github.com>2022-06-04 13:49:52 +0200
commite49868c2bda9708c92a35474262ddfe14af0fdc9 (patch)
treeadd690ede408dd75bb682a64a993e1d33d387378
parent8ab9ab5c73f781871e320a65f73c778fe6b2a8eb (diff)
downloadpylint-git-e49868c2bda9708c92a35474262ddfe14af0fdc9.tar.gz
Store namespaces respective to directories (#6789)
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
-rw-r--r--.pyenchant_pylint_custom_dict.txt1
-rw-r--r--pylint/config/arguments_manager.py13
-rw-r--r--pylint/config/config_initialization.py3
-rw-r--r--pylint/lint/pylinter.py24
-rw-r--r--pylint/lint/utils.py13
-rw-r--r--pylint/typing.py4
-rw-r--r--tests/config/test_per_directory_config.py24
7 files changed, 81 insertions, 1 deletions
diff --git a/.pyenchant_pylint_custom_dict.txt b/.pyenchant_pylint_custom_dict.txt
index b5d832d05..80918ac5a 100644
--- a/.pyenchant_pylint_custom_dict.txt
+++ b/.pyenchant_pylint_custom_dict.txt
@@ -21,6 +21,7 @@ async
asynccontextmanager
attr
attrib
+backport
BaseChecker
basename
behaviour
diff --git a/pylint/config/arguments_manager.py b/pylint/config/arguments_manager.py
index 301769e77..eda1a583d 100644
--- a/pylint/config/arguments_manager.py
+++ b/pylint/config/arguments_manager.py
@@ -42,7 +42,7 @@ from pylint.config.option_parser import OptionParser
from pylint.config.options_provider_mixin import OptionsProviderMixIn
from pylint.config.utils import _convert_option_to_argument, _parse_rich_type_value
from pylint.constants import MAIN_CHECKER_NAME
-from pylint.typing import OptionDict
+from pylint.typing import DirectoryNamespaceDict, OptionDict
if sys.version_info >= (3, 11):
import tomllib
@@ -66,6 +66,14 @@ class _ArgumentsManager:
self._config = argparse.Namespace()
"""Namespace for all options."""
+ self._base_config = self._config
+ """Fall back Namespace object created during initialization.
+
+ This is necessary for the per-directory configuration support. Whenever we
+ fail to match a file with a directory we fall back to the Namespace object
+ created during initialization.
+ """
+
self._arg_parser = argparse.ArgumentParser(
prog=prog,
usage=usage or "%(prog)s [options]",
@@ -82,6 +90,9 @@ class _ArgumentsManager:
self._option_dicts: dict[str, OptionDict] = {}
"""All option dictionaries that have been registered."""
+ self._directory_namespaces: DirectoryNamespaceDict = {}
+ """Mapping of directories and their respective namespace objects."""
+
# TODO: 3.0: Remove deprecated attributes introduced to keep API
# parity with optparse. Until '_maxlevel'
with warnings.catch_warnings():
diff --git a/pylint/config/config_initialization.py b/pylint/config/config_initialization.py
index b5bd31234..7c67d08d6 100644
--- a/pylint/config/config_initialization.py
+++ b/pylint/config/config_initialization.py
@@ -106,6 +106,9 @@ def _config_initialization(
linter._parse_error_mode()
+ # Link the base Namespace object on the current directory
+ linter._directory_namespaces[Path(".").resolve()] = (linter.config, {})
+
# parsed_args_list should now only be a list of files/directories to lint.
# All other options have been removed from the list.
return parsed_args_list
diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py
index 63eb84e10..625ab07f6 100644
--- a/pylint/lint/pylinter.py
+++ b/pylint/lint/pylinter.py
@@ -4,6 +4,7 @@
from __future__ import annotations
+import argparse
import collections
import contextlib
import functools
@@ -15,6 +16,7 @@ import warnings
from collections import defaultdict
from collections.abc import Callable, Iterable, Iterator, Sequence
from io import TextIOWrapper
+from pathlib import Path
from typing import Any
import astroid
@@ -40,6 +42,7 @@ from pylint.lint.report_functions import (
report_total_messages_stats,
)
from pylint.lint.utils import (
+ _is_relative_to,
fix_import_path,
get_fatal_error_message,
prepare_crash_report,
@@ -49,6 +52,7 @@ from pylint.reporters.base_reporter import BaseReporter
from pylint.reporters.text import TextReporter
from pylint.reporters.ureports import nodes as report_nodes
from pylint.typing import (
+ DirectoryNamespaceDict,
FileItem,
ManagedMessage,
MessageDefinitionTuple,
@@ -791,6 +795,26 @@ class PyLinter(
self.current_file = filepath or modname
self.stats.init_single_module(modname or "")
+ # If there is an actual filepath we might need to update the config attribute
+ if filepath:
+ namespace = self._get_namespace_for_file(
+ Path(filepath), self._directory_namespaces
+ )
+ if namespace:
+ self.config = namespace or self._base_config
+
+ def _get_namespace_for_file(
+ self, filepath: Path, namespaces: DirectoryNamespaceDict
+ ) -> argparse.Namespace | None:
+ for directory in namespaces:
+ if _is_relative_to(filepath, directory):
+ namespace = self._get_namespace_for_file(
+ filepath, namespaces[directory][1]
+ )
+ if namespace is None:
+ return namespaces[directory][0]
+ return None
+
@contextlib.contextmanager
def _astroid_module_checker(
self,
diff --git a/pylint/lint/utils.py b/pylint/lint/utils.py
index 30694c25c..ff2812e7e 100644
--- a/pylint/lint/utils.py
+++ b/pylint/lint/utils.py
@@ -99,3 +99,16 @@ def fix_import_path(args: Sequence[str]) -> Iterator[None]:
yield
finally:
sys.path[:] = original
+
+
+def _is_relative_to(self: Path, *other: Path) -> bool:
+ """Checks if self is relative to other.
+
+ Backport of pathlib.Path.is_relative_to for Python <3.9
+ TODO: py39: Remove this backport and use stdlib function.
+ """
+ try:
+ self.relative_to(*other)
+ return True
+ except ValueError:
+ return False
diff --git a/pylint/typing.py b/pylint/typing.py
index f5650c1e7..7ceb33f91 100644
--- a/pylint/typing.py
+++ b/pylint/typing.py
@@ -6,7 +6,9 @@
from __future__ import annotations
+import argparse
import sys
+from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
@@ -127,3 +129,5 @@ MessageDefinitionTuple = Union[
Tuple[str, str, str],
Tuple[str, str, str, ExtraMessageOptions],
]
+# Mypy doesn't support recursive types (yet), see https://github.com/python/mypy/issues/731
+DirectoryNamespaceDict = Dict[Path, Tuple[argparse.Namespace, "DirectoryNamespaceDict"]] # type: ignore[misc]
diff --git a/tests/config/test_per_directory_config.py b/tests/config/test_per_directory_config.py
new file mode 100644
index 000000000..9bc0ef9bc
--- /dev/null
+++ b/tests/config/test_per_directory_config.py
@@ -0,0 +1,24 @@
+# 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
+
+
+from py._path.local import LocalPath # type: ignore[import]
+
+from pylint.lint import Run
+
+
+def test_fall_back_on_base_config(tmpdir: LocalPath) -> None:
+ """Test that we correctly fall back on the base config."""
+ # A file under the current dir should fall back to the highest level
+ # For pylint this is ./pylintrc
+ test_file = tmpdir / "test.py"
+ runner = Run([__name__], exit=False)
+ assert id(runner.linter.config) == id(runner.linter._base_config)
+
+ # When the file is a directory that does not have any of its parents in
+ # linter._directory_namespaces it should default to the base config
+ with open(test_file, "w", encoding="utf-8") as f:
+ f.write("1")
+ Run([str(test_file)], exit=False)
+ assert id(runner.linter.config) == id(runner.linter._base_config)