7
7
import argparse
8
8
import collections
9
9
import contextlib
10
+ import copy
10
11
import functools
11
12
import os
12
13
import sys
26
27
from pylint import checkers , exceptions , interfaces , reporters
27
28
from pylint .checkers .base_checker import BaseChecker
28
29
from pylint .config .arguments_manager import _ArgumentsManager
30
+ from pylint .config .config_initialization import _config_initialization
31
+ from pylint .config .find_default_config_files import find_subdirectory_config_files
29
32
from pylint .constants import (
30
33
MAIN_CHECKER_NAME ,
31
34
MSG_TYPES ,
@@ -615,6 +618,43 @@ def initialize(self) -> None:
615
618
if not msg .may_be_emitted (self .config .py_version ):
616
619
self ._msgs_state [msg .msgid ] = False
617
620
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
+ # in order to avoid creating new PyLinter objects, _config_initialization modifies
641
+ # existing self.config, so we need to save original self.config to restore it later
642
+ original_config_ref = self .config
643
+ self .config = copy .deepcopy (self .config )
644
+ _config_initialization (self , self ._cli_args , config_file = local_conf )
645
+ self ._directory_namespaces [basedir .resolve ()] = (self .config , {})
646
+ # keep dict keys reverse-sorted so that
647
+ # iteration over keys in _get_namespace_for_file gets the most nested path first
648
+ self ._directory_namespaces = dict (
649
+ sorted (self ._directory_namespaces .items (), reverse = True )
650
+ )
651
+ self .config = original_config_ref
652
+ break
653
+ if basedir .parent != basedir :
654
+ basedir = basedir .parent
655
+ else :
656
+ break
657
+
618
658
def _discover_files (self , files_or_modules : Sequence [str ]) -> Iterator [str ]:
619
659
"""Discover python modules and packages in sub-directory.
620
660
@@ -665,12 +705,12 @@ def check(self, files_or_modules: Sequence[str]) -> None:
665
705
"Missing filename required for --from-stdin"
666
706
)
667
707
668
- extra_packages_paths = list (
669
- {
708
+ extra_packages_paths_set = set ()
709
+ for file_or_module in files_or_modules :
710
+ extra_packages_paths_set .add (
670
711
discover_package_path (file_or_module , self .config .source_roots )
671
- for file_or_module in files_or_modules
672
- }
673
- )
712
+ )
713
+ extra_packages_paths = list (extra_packages_paths_set )
674
714
675
715
# TODO: Move the parallel invocation into step 3 of the checking process
676
716
if not self .config .from_stdin and self .config .jobs > 1 :
@@ -693,14 +733,16 @@ def check(self, files_or_modules: Sequence[str]) -> None:
693
733
fileitems = self ._iterate_file_descrs (files_or_modules )
694
734
data = None
695
735
696
- # The contextmanager also opens all checkers and sets up the PyLinter class
697
736
with augmented_sys_path (extra_packages_paths ):
698
- 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
703
- self ._lint_files (ast_per_fileitem , check_astroid_module )
737
+ # 2) Get the AST for each FileItem
738
+ ast_per_fileitem = self ._get_asts (fileitems , data )
739
+ # 3) Lint each ast
740
+ if self .config .use_local_configs is False :
741
+ # The contextmanager also opens all checkers and sets up the PyLinter class
742
+ with self ._astroid_module_checker () as check_astroid_module :
743
+ self ._lint_files (ast_per_fileitem , check_astroid_module )
744
+ else :
745
+ self ._lint_files (ast_per_fileitem , None )
704
746
705
747
def _get_asts (
706
748
self , fileitems : Iterator [FileItem ], data : str | None
@@ -710,6 +752,7 @@ def _get_asts(
710
752
711
753
for fileitem in fileitems :
712
754
self .set_current_module (fileitem .name , fileitem .filepath )
755
+ self ._set_astroid_options ()
713
756
714
757
try :
715
758
ast_per_fileitem [fileitem ] = self .get_ast (
@@ -741,7 +784,7 @@ def check_single_file_item(self, file: FileItem) -> None:
741
784
def _lint_files (
742
785
self ,
743
786
ast_mapping : dict [FileItem , nodes .Module | None ],
744
- check_astroid_module : Callable [[nodes .Module ], bool | None ],
787
+ check_astroid_module : Callable [[nodes .Module ], bool | None ] | None ,
745
788
) -> None :
746
789
"""Lint all AST modules from a mapping.."""
747
790
for fileitem , module in ast_mapping .items ():
@@ -765,7 +808,7 @@ def _lint_file(
765
808
self ,
766
809
file : FileItem ,
767
810
module : nodes .Module ,
768
- check_astroid_module : Callable [[nodes .Module ], bool | None ],
811
+ check_astroid_module : Callable [[nodes .Module ], bool | None ] | None ,
769
812
) -> None :
770
813
"""Lint a file using the passed utility function check_astroid_module).
771
814
@@ -784,7 +827,13 @@ def _lint_file(
784
827
self .current_file = module .file
785
828
786
829
try :
787
- check_astroid_module (module )
830
+ # call _astroid_module_checker after set_current_module, when
831
+ # self.config is the right config for current module
832
+ if check_astroid_module is None :
833
+ with self ._astroid_module_checker () as local_check_astroid_module :
834
+ local_check_astroid_module (module )
835
+ else :
836
+ check_astroid_module (module )
788
837
except Exception as e :
789
838
raise astroid .AstroidError from e
790
839
@@ -907,7 +956,8 @@ def set_current_module(self, modname: str, filepath: str | None = None) -> None:
907
956
self .stats .init_single_module (modname or "" )
908
957
909
958
# If there is an actual filepath we might need to update the config attribute
910
- if filepath :
959
+ if filepath and self .config .use_local_configs :
960
+ self .register_local_config (filepath )
911
961
namespace = self ._get_namespace_for_file (
912
962
Path (filepath ), self ._directory_namespaces
913
963
)
@@ -917,6 +967,7 @@ def set_current_module(self, modname: str, filepath: str | None = None) -> None:
917
967
def _get_namespace_for_file (
918
968
self , filepath : Path , namespaces : DirectoryNamespaceDict
919
969
) -> argparse .Namespace | None :
970
+ filepath = filepath .resolve ()
920
971
for directory in namespaces :
921
972
if _is_relative_to (filepath , directory ):
922
973
namespace = self ._get_namespace_for_file (
@@ -1068,16 +1119,19 @@ def _check_astroid_module(
1068
1119
walker .walk (node )
1069
1120
return True
1070
1121
1071
- def open (self ) -> None :
1072
- """Initialize counters ."""
1122
+ def _set_astroid_options (self ) -> None :
1123
+ """Pass some config values to astroid.MANAGER object ."""
1073
1124
MANAGER .always_load_extensions = self .config .unsafe_load_any_extension
1074
1125
MANAGER .max_inferable_values = self .config .limit_inference_results
1075
1126
MANAGER .extension_package_whitelist .update (self .config .extension_pkg_allow_list )
1076
1127
if self .config .extension_pkg_whitelist :
1077
1128
MANAGER .extension_package_whitelist .update (
1078
1129
self .config .extension_pkg_whitelist
1079
1130
)
1080
- self .stats .reset_message_count ()
1131
+
1132
+ def open (self ) -> None :
1133
+ """Initialize self as main checker for one or more modules."""
1134
+ self ._set_astroid_options ()
1081
1135
1082
1136
def generate_reports (self , verbose : bool = False ) -> int | None :
1083
1137
"""Close the whole package /module, it's time to make reports !
0 commit comments