Skip to content

Commit

Permalink
Create additional Namespaces for subdirectories with configuration files
Browse files Browse the repository at this point in the history
  • Loading branch information
Aleksey Petryankin committed Jan 28, 2024
1 parent 1ee9bc1 commit 71fc7c3
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 10 deletions.
5 changes: 5 additions & 0 deletions pylint/config/arguments_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ def __init__(
self._directory_namespaces: DirectoryNamespaceDict = {}
"""Mapping of directories and their respective namespace objects."""

self._cli_args: list[str] = []
"""Options that were passed as command line arguments and have highest priority."""

@property
def config(self) -> argparse.Namespace:
"""Namespace for all options."""
Expand Down Expand Up @@ -226,6 +229,8 @@ def _parse_command_line_configuration(
) -> list[str]:
"""Parse the arguments found on the command line into the namespace."""
arguments = sys.argv[1:] if arguments is None else arguments
if not self._cli_args:
self._cli_args = list(arguments)

self.config, parsed_args = self._arg_parser.parse_known_args(
arguments, self.config
Expand Down
17 changes: 12 additions & 5 deletions pylint/config/find_default_config_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,19 @@ def _cfg_has_config(path: Path | str) -> bool:
return any(section.startswith("pylint.") for section in parser.sections())


def _yield_default_files() -> Iterator[Path]:
def _yield_default_files(basedir: Path | str = ".") -> Iterator[Path]:
"""Iterate over the default config file names and see if they exist."""
basedir = Path(basedir)
for config_name in CONFIG_NAMES:
config_file = basedir / config_name
try:
if config_name.is_file():
if config_name.suffix == ".toml" and not _toml_has_config(config_name):
if config_file.is_file():
if config_file.suffix == ".toml" and not _toml_has_config(config_file):
continue
if config_name.suffix == ".cfg" and not _cfg_has_config(config_name):
if config_file.suffix == ".cfg" and not _cfg_has_config(config_file):
continue

yield config_name.resolve()
yield config_file.resolve()
except OSError:
pass

Expand Down Expand Up @@ -142,3 +144,8 @@ def find_default_config_files() -> Iterator[Path]:
yield Path("/etc/pylintrc").resolve()
except OSError:
pass


def find_subdirectory_config_files(basedir: Path | str) -> Iterator[Path]:
"""Find config file in arbitrary subdirectory."""
yield from _yield_default_files(basedir)
19 changes: 19 additions & 0 deletions pylint/lint/base_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,25 @@ def _make_linter_options(linter: PyLinter) -> Options:
"Useful if running pylint in a server-like mode.",
},
),
(
"use-local-configs",
{
"default": False,
"type": "yn",
"metavar": "<y or n>",
"help": "When some of the linted files or modules have pylint config in the same directory, "
"use their local configs for checking these files.",
},
),
(
"use-parent-configs",
{
"default": False,
"type": "yn",
"metavar": "<y or n>",
"help": "Search for local pylint configs up until current working directory or root.",
},
),
)


Expand Down
46 changes: 41 additions & 5 deletions pylint/lint/pylinter.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
from pylint import checkers, exceptions, interfaces, reporters
from pylint.checkers.base_checker import BaseChecker
from pylint.config.arguments_manager import _ArgumentsManager
from pylint.config.config_initialization import _config_initialization
from pylint.config.find_default_config_files import find_subdirectory_config_files
from pylint.constants import (
MAIN_CHECKER_NAME,
MSG_TYPES,
Expand Down Expand Up @@ -616,6 +618,37 @@ def initialize(self) -> None:
if not msg.may_be_emitted(self.config.py_version):
self._msgs_state[msg.msgid] = False

def register_local_config(self, file_or_dir: str) -> None:
if os.path.isdir(file_or_dir):
basedir = Path(file_or_dir)
else:
basedir = Path(os.path.dirname(file_or_dir))

if self.config.use_parent_configs is False:
# exit loop after first iteration
scan_root_dir = basedir
elif _is_relative_to(basedir, Path(os.getcwd())):
scan_root_dir = Path(os.getcwd())
else:
scan_root_dir = Path("/")

while basedir.resolve() not in self._directory_namespaces and _is_relative_to(
basedir, scan_root_dir
):
local_conf = next(find_subdirectory_config_files(basedir), None)
if local_conf is not None:
_config_initialization(self, self._cli_args, config_file=local_conf)
self._directory_namespaces[Path(basedir).resolve()] = (
self.config,
{},
)
self.config = self._base_config
break
if basedir.parent != basedir:
basedir = basedir.parent
else:
break

def _discover_files(self, files_or_modules: Sequence[str]) -> Iterator[str]:
"""Discover python modules and packages in sub-directory.
Expand Down Expand Up @@ -666,12 +699,15 @@ def check(self, files_or_modules: Sequence[str]) -> None:
"Missing filename required for --from-stdin"
)

extra_packages_paths = list(
{
if self.config.use_local_configs is True:
for file_or_module in files_or_modules:
self.register_local_config(file_or_module)
extra_packages_paths_set = set()
for file_or_module in files_or_modules:
extra_packages_paths_set.add(
discover_package_path(file_or_module, self.config.source_roots)
for file_or_module in files_or_modules
}
)
)
extra_packages_paths = list(extra_packages_paths_set)

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

0 comments on commit 71fc7c3

Please sign in to comment.