Skip to content

Commit d603f94

Browse files
author
Aleksey Petryankin
committed
Create additional Namespaces for subdirectories with configuration files
Closes pylint-dev#618
1 parent 2aef92f commit d603f94

File tree

4 files changed

+77
-10
lines changed

4 files changed

+77
-10
lines changed

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/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/base_options.py

+19
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,25 @@ def _make_linter_options(linter: PyLinter) -> Options:
414414
"Useful if running pylint in a server-like mode.",
415415
},
416416
),
417+
(
418+
"use-local-configs",
419+
{
420+
"default": False,
421+
"type": "yn",
422+
"metavar": "<y or n>",
423+
"help": "When some of the linted files or modules have pylint config in the same directory, "
424+
"use their local configs for checking these files.",
425+
},
426+
),
427+
(
428+
"use-parent-configs",
429+
{
430+
"default": False,
431+
"type": "yn",
432+
"metavar": "<y or n>",
433+
"help": "Search for local pylint configs up until current working directory or root.",
434+
},
435+
),
417436
)
418437

419438

pylint/lint/pylinter.py

+41-5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
from pylint import checkers, exceptions, interfaces, reporters
2727
from pylint.checkers.base_checker import BaseChecker
2828
from pylint.config.arguments_manager import _ArgumentsManager
29+
from pylint.config.config_initialization import _config_initialization
30+
from pylint.config.find_default_config_files import find_subdirectory_config_files
2931
from pylint.constants import (
3032
MAIN_CHECKER_NAME,
3133
MSG_TYPES,
@@ -616,6 +618,37 @@ def initialize(self) -> None:
616618
if not msg.may_be_emitted(self.config.py_version):
617619
self._msgs_state[msg.msgid] = False
618620

621+
def register_local_config(self, file_or_dir: str) -> None:
622+
if os.path.isdir(file_or_dir):
623+
basedir = Path(file_or_dir)
624+
else:
625+
basedir = Path(os.path.dirname(file_or_dir))
626+
627+
if self.config.use_parent_configs is False:
628+
# exit loop after first iteration
629+
scan_root_dir = basedir
630+
elif _is_relative_to(basedir, Path(os.getcwd())):
631+
scan_root_dir = Path(os.getcwd())
632+
else:
633+
scan_root_dir = Path("/")
634+
635+
while basedir.resolve() not in self._directory_namespaces and _is_relative_to(
636+
basedir, scan_root_dir
637+
):
638+
local_conf = next(find_subdirectory_config_files(basedir), None)
639+
if local_conf is not None:
640+
_config_initialization(self, self._cli_args, config_file=local_conf)
641+
self._directory_namespaces[Path(basedir).resolve()] = (
642+
self.config,
643+
{},
644+
)
645+
self.config = self._base_config
646+
break
647+
if basedir.parent != basedir:
648+
basedir = basedir.parent
649+
else:
650+
break
651+
619652
def _discover_files(self, files_or_modules: Sequence[str]) -> Iterator[str]:
620653
"""Discover python modules and packages in sub-directory.
621654
@@ -666,12 +699,15 @@ def check(self, files_or_modules: Sequence[str]) -> None:
666699
"Missing filename required for --from-stdin"
667700
)
668701

669-
extra_packages_paths = list(
670-
{
702+
if self.config.use_local_configs is True:
703+
for file_or_module in files_or_modules:
704+
self.register_local_config(file_or_module)
705+
extra_packages_paths_set = set()
706+
for file_or_module in files_or_modules:
707+
extra_packages_paths_set.add(
671708
discover_package_path(file_or_module, self.config.source_roots)
672-
for file_or_module in files_or_modules
673-
}
674-
)
709+
)
710+
extra_packages_paths = list(extra_packages_paths_set)
675711

676712
# TODO: Move the parallel invocation into step 3 of the checking process
677713
if not self.config.from_stdin and self.config.jobs > 1:

0 commit comments

Comments
 (0)