Skip to content

Commit 04d878c

Browse files
author
Aleksey Petryankin
committed
Prepare code for per-directory configuration files
- Add opportunity to open checkers per-file, so they can use values from local config during opening - Save command line arguments to apply them on top of each new config - More accurate verbose messages about config files - Enable finding config files in arbitrary directories - Add opportunity to call linter._astroid_module_checker per file in single-process mode - Collect stats from several calls of linter._astroid_module_checker in single-process mode - Extend linter._get_namespace_for_file to return the path from which namespace was created
1 parent e1a88bf commit 04d878c

6 files changed

+79
-31
lines changed

.pyenchant_pylint_custom_dict.txt

+3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ codecs
6161
col's
6262
conf
6363
config
64+
configs
6465
const
6566
Const
6667
contextlib
@@ -310,11 +311,13 @@ str
310311
stringified
311312
subclasses
312313
subcommands
314+
subconfigs
313315
subdicts
314316
subgraphs
315317
sublists
316318
submodule
317319
submodules
320+
subpackage
318321
subparsers
319322
subparts
320323
subprocess

pylint/config/arguments_manager.py

+5
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ def __init__(
8181
self._directory_namespaces: DirectoryNamespaceDict = {}
8282
"""Mapping of directories and their respective namespace objects."""
8383

84+
self._cli_args: list[str] = []
85+
"""Options that were passed as command line arguments and have highest priority."""
86+
8487
@property
8588
def config(self) -> argparse.Namespace:
8689
"""Namespace for all options."""
@@ -226,6 +229,8 @@ def _parse_command_line_configuration(
226229
) -> list[str]:
227230
"""Parse the arguments found on the command line into the namespace."""
228231
arguments = sys.argv[1:] if arguments is None else arguments
232+
if not self._cli_args:
233+
self._cli_args = list(arguments)
229234

230235
self.config, parsed_args = self._arg_parser.parse_known_args(
231236
arguments, self.config

pylint/config/config_file_parser.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def parse_config_file(
106106
raise OSError(f"The config file {file_path} doesn't exist!")
107107

108108
if verbose:
109-
print(f"Using config file {file_path}", file=sys.stderr)
109+
print(f"Loading config file {file_path}", file=sys.stderr)
110110

111111
if file_path.suffix == ".toml":
112112
return _RawConfParser.parse_toml_file(file_path)

pylint/config/config_initialization.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from pylint.lint import PyLinter
2424

2525

26+
# pylint: disable = too-many-statements
2627
def _config_initialization(
2728
linter: PyLinter,
2829
args_list: list[str],
@@ -82,6 +83,9 @@ def _config_initialization(
8283
args_list = _order_all_first(args_list, joined=True)
8384
parsed_args_list = linter._parse_command_line_configuration(args_list)
8485

86+
# save Runner.verbose to make this preprocessed option visible from other modules
87+
linter.config.verbose = verbose_mode
88+
8589
# Remove the positional arguments separator from the list of arguments if it exists
8690
try:
8791
parsed_args_list.remove("--")
@@ -141,7 +145,8 @@ def _config_initialization(
141145
linter._parse_error_mode()
142146

143147
# Link the base Namespace object on the current directory
144-
linter._directory_namespaces[Path(".").resolve()] = (linter.config, {})
148+
if Path(".").resolve() not in linter._directory_namespaces:
149+
linter._directory_namespaces[Path(".").resolve()] = (linter.config, {})
145150

146151
# parsed_args_list should now only be a list of inputs to lint.
147152
# All other options have been removed from the list.

pylint/config/find_default_config_files.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,19 @@ def _cfg_has_config(path: Path | str) -> bool:
6464
return any(section.startswith("pylint.") for section in parser.sections())
6565

6666

67-
def _yield_default_files() -> Iterator[Path]:
67+
def _yield_default_files(basedir: Path | str = ".") -> Iterator[Path]:
6868
"""Iterate over the default config file names and see if they exist."""
69+
basedir = Path(basedir)
6970
for config_name in CONFIG_NAMES:
71+
config_file = basedir / config_name
7072
try:
71-
if config_name.is_file():
72-
if config_name.suffix == ".toml" and not _toml_has_config(config_name):
73+
if config_file.is_file():
74+
if config_file.suffix == ".toml" and not _toml_has_config(config_file):
7375
continue
74-
if config_name.suffix == ".cfg" and not _cfg_has_config(config_name):
76+
if config_file.suffix == ".cfg" and not _cfg_has_config(config_file):
7577
continue
7678

77-
yield config_name.resolve()
79+
yield config_file.resolve()
7880
except OSError:
7981
pass
8082

@@ -142,3 +144,8 @@ def find_default_config_files() -> Iterator[Path]:
142144
yield Path("/etc/pylintrc").resolve()
143145
except OSError:
144146
pass
147+
148+
149+
def find_subdirectory_config_files(basedir: Path | str) -> Iterator[Path]:
150+
"""Find config file in arbitrary subdirectory."""
151+
yield from _yield_default_files(basedir)

pylint/lint/pylinter.py

+52-24
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
ModuleDescriptionDict,
6767
Options,
6868
)
69-
from pylint.utils import ASTWalker, FileState, LinterStats, utils
69+
from pylint.utils import ASTWalker, FileState, LinterStats, merge_stats, utils
7070

7171
MANAGER = astroid.MANAGER
7272

@@ -317,6 +317,7 @@ def __init__(
317317

318318
# Attributes related to stats
319319
self.stats = LinterStats()
320+
self.all_stats: list[LinterStats] = []
320321

321322
# Attributes related to (command-line) options and their parsing
322323
self.options: Options = options + _make_linter_options(self)
@@ -665,12 +666,12 @@ def check(self, files_or_modules: Sequence[str]) -> None:
665666
"Missing filename required for --from-stdin"
666667
)
667668

668-
extra_packages_paths = list(
669-
{
669+
extra_packages_paths_set = set()
670+
for file_or_module in files_or_modules:
671+
extra_packages_paths_set.add(
670672
discover_package_path(file_or_module, self.config.source_roots)
671-
for file_or_module in files_or_modules
672-
}
673-
)
673+
)
674+
extra_packages_paths = list(extra_packages_paths_set)
674675

675676
# TODO: Move the parallel invocation into step 3 of the checking process
676677
if not self.config.from_stdin and self.config.jobs > 1:
@@ -693,13 +694,12 @@ def check(self, files_or_modules: Sequence[str]) -> None:
693694
fileitems = self._iterate_file_descrs(files_or_modules)
694695
data = None
695696

696-
# The contextmanager also opens all checkers and sets up the PyLinter class
697697
with augmented_sys_path(extra_packages_paths):
698+
# 2) Get the AST for each FileItem
699+
ast_per_fileitem = self._get_asts(fileitems, data)
700+
# 3) Lint each ast
701+
# The contextmanager also opens all checkers and sets up the PyLinter class
698702
with self._astroid_module_checker() as check_astroid_module:
699-
# 2) Get the AST for each FileItem
700-
ast_per_fileitem = self._get_asts(fileitems, data)
701-
702-
# 3) Lint each ast
703703
self._lint_files(ast_per_fileitem, check_astroid_module)
704704

705705
def _get_asts(
@@ -710,6 +710,7 @@ def _get_asts(
710710

711711
for fileitem in fileitems:
712712
self.set_current_module(fileitem.name, fileitem.filepath)
713+
self._set_astroid_options()
713714

714715
try:
715716
ast_per_fileitem[fileitem] = self.get_ast(
@@ -735,13 +736,14 @@ def check_single_file_item(self, file: FileItem) -> None:
735736
736737
initialize() should be called before calling this method
737738
"""
739+
self.set_current_module(file.name, file.filepath)
738740
with self._astroid_module_checker() as check_astroid_module:
739741
self._check_file(self.get_ast, check_astroid_module, file)
740742

741743
def _lint_files(
742744
self,
743745
ast_mapping: dict[FileItem, nodes.Module | None],
744-
check_astroid_module: Callable[[nodes.Module], bool | None],
746+
check_astroid_module: Callable[[nodes.Module], bool | None] | None,
745747
) -> None:
746748
"""Lint all AST modules from a mapping.."""
747749
for fileitem, module in ast_mapping.items():
@@ -760,12 +762,17 @@ def _lint_files(
760762
)
761763
else:
762764
self.add_message("fatal", args=msg, confidence=HIGH)
765+
# current self.stats is needed in merge - it contains stats from last module
766+
finished_run_stats = merge_stats([*self.all_stats, self.stats])
767+
# after _lint_files linter.stats is aggregate stats from all modules, like after check_parallel
768+
self.all_stats = []
769+
self.stats = finished_run_stats
763770

764771
def _lint_file(
765772
self,
766773
file: FileItem,
767774
module: nodes.Module,
768-
check_astroid_module: Callable[[nodes.Module], bool | None],
775+
check_astroid_module: Callable[[nodes.Module], bool | None] | None,
769776
) -> None:
770777
"""Lint a file using the passed utility function check_astroid_module).
771778
@@ -784,7 +791,13 @@ def _lint_file(
784791
self.current_file = module.file
785792

786793
try:
787-
check_astroid_module(module)
794+
# call _astroid_module_checker after set_current_module, when
795+
# self.config is the right config for current module
796+
if check_astroid_module is None:
797+
with self._astroid_module_checker() as local_check_astroid_module:
798+
local_check_astroid_module(module)
799+
else:
800+
check_astroid_module(module)
788801
except Exception as e:
789802
raise astroid.AstroidError from e
790803

@@ -898,33 +911,44 @@ def _expand_files(
898911
def set_current_module(self, modname: str, filepath: str | None = None) -> None:
899912
"""Set the name of the currently analyzed module and
900913
init statistics for it.
914+
915+
Save current stats before init to make sure no counters for
916+
error, statement, etc are missed.
901917
"""
902918
if not modname and filepath is None:
903919
return
904920
self.reporter.on_set_current_module(modname or "", filepath)
905921
self.current_name = modname
906922
self.current_file = filepath or modname
923+
self.all_stats.append(self.stats)
924+
self.stats = LinterStats()
907925
self.stats.init_single_module(modname or "")
908926

909927
# If there is an actual filepath we might need to update the config attribute
910928
if filepath:
911-
namespace = self._get_namespace_for_file(
929+
config_path, namespace = self._get_namespace_for_file(
912930
Path(filepath), self._directory_namespaces
913931
)
914932
if namespace:
915-
self.config = namespace or self._base_config
933+
self.config = namespace
934+
if self.config.verbose:
935+
print(
936+
f"Using config file from {config_path} for {filepath}",
937+
file=sys.stderr,
938+
)
916939

917940
def _get_namespace_for_file(
918941
self, filepath: Path, namespaces: DirectoryNamespaceDict
919-
) -> argparse.Namespace | None:
942+
) -> tuple[Path | None, argparse.Namespace | None]:
943+
filepath = filepath.resolve()
920944
for directory in namespaces:
921945
if _is_relative_to(filepath, directory):
922-
namespace = self._get_namespace_for_file(
946+
_, namespace = self._get_namespace_for_file(
923947
filepath, namespaces[directory][1]
924948
)
925949
if namespace is None:
926-
return namespaces[directory][0]
927-
return None
950+
return directory, namespaces[directory][0]
951+
return None, None
928952

929953
@contextlib.contextmanager
930954
def _astroid_module_checker(
@@ -953,7 +977,7 @@ def _astroid_module_checker(
953977
rawcheckers=rawcheckers,
954978
)
955979

956-
# notify global end
980+
# notify end of module if jobs>1, global end otherwise
957981
self.stats.statement = walker.nbstatements
958982
for checker in reversed(_checkers):
959983
checker.close()
@@ -1068,22 +1092,26 @@ def _check_astroid_module(
10681092
walker.walk(node)
10691093
return True
10701094

1071-
def open(self) -> None:
1072-
"""Initialize counters."""
1095+
def _set_astroid_options(self) -> None:
1096+
"""Pass some config values to astroid.MANAGER object."""
10731097
MANAGER.always_load_extensions = self.config.unsafe_load_any_extension
10741098
MANAGER.max_inferable_values = self.config.limit_inference_results
10751099
MANAGER.extension_package_whitelist.update(self.config.extension_pkg_allow_list)
10761100
if self.config.extension_pkg_whitelist:
10771101
MANAGER.extension_package_whitelist.update(
10781102
self.config.extension_pkg_whitelist
10791103
)
1080-
self.stats.reset_message_count()
1104+
1105+
def open(self) -> None:
1106+
"""Initialize self as main checker for one or more modules."""
1107+
self._set_astroid_options()
10811108

10821109
def generate_reports(self, verbose: bool = False) -> int | None:
10831110
"""Close the whole package /module, it's time to make reports !
10841111
10851112
if persistent run, pickle results for later comparison
10861113
"""
1114+
self.config = self._base_config
10871115
# Display whatever messages are left on the reporter.
10881116
self.reporter.display_messages(report_nodes.Section())
10891117
if not self.file_state._is_base_filestate:

0 commit comments

Comments
 (0)