diff options
author | Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com> | 2022-06-04 13:49:52 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-06-04 13:49:52 +0200 |
commit | e49868c2bda9708c92a35474262ddfe14af0fdc9 (patch) | |
tree | add690ede408dd75bb682a64a993e1d33d387378 | |
parent | 8ab9ab5c73f781871e320a65f73c778fe6b2a8eb (diff) | |
download | pylint-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.txt | 1 | ||||
-rw-r--r-- | pylint/config/arguments_manager.py | 13 | ||||
-rw-r--r-- | pylint/config/config_initialization.py | 3 | ||||
-rw-r--r-- | pylint/lint/pylinter.py | 24 | ||||
-rw-r--r-- | pylint/lint/utils.py | 13 | ||||
-rw-r--r-- | pylint/typing.py | 4 | ||||
-rw-r--r-- | tests/config/test_per_directory_config.py | 24 |
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) |