1"""Config handling logic for Flake8.""" 2import collections 3import configparser 4import logging 5import os.path 6from typing import List 7from typing import Optional 8from typing import Tuple 9 10from flake8 import utils 11 12LOG = logging.getLogger(__name__) 13 14__all__ = ("ConfigFileFinder", "ConfigParser") 15 16 17class ConfigFileFinder: 18 """Encapsulate the logic for finding and reading config files.""" 19 20 def __init__( 21 self, 22 program_name: str, 23 extra_config_files: Optional[List[str]] = None, 24 config_file: Optional[str] = None, 25 ignore_config_files: bool = False, 26 ) -> None: 27 """Initialize object to find config files. 28 29 :param str program_name: 30 Name of the current program (e.g., flake8). 31 :param list extra_config_files: 32 Extra configuration files specified by the user to read. 33 :param str config_file: 34 Configuration file override to only read configuration from. 35 :param bool ignore_config_files: 36 Determine whether to ignore configuration files or not. 37 """ 38 # The values of --append-config from the CLI 39 if extra_config_files is None: 40 extra_config_files = [] 41 self.extra_config_files = utils.normalize_paths(extra_config_files) 42 43 # The value of --config from the CLI. 44 self.config_file = config_file 45 46 # The value of --isolated from the CLI. 47 self.ignore_config_files = ignore_config_files 48 49 # User configuration file. 50 self.program_name = program_name 51 52 # List of filenames to find in the local/project directory 53 self.project_filenames = ("setup.cfg", "tox.ini", f".{program_name}") 54 55 self.local_directory = os.path.abspath(os.curdir) 56 57 @staticmethod 58 def _read_config( 59 *files: str, 60 ) -> Tuple[configparser.RawConfigParser, List[str]]: 61 config = configparser.RawConfigParser() 62 63 found_files = [] 64 for filename in files: 65 try: 66 found_files.extend(config.read(filename)) 67 except UnicodeDecodeError: 68 LOG.exception( 69 "There was an error decoding a config file." 70 "The file with a problem was %s.", 71 filename, 72 ) 73 except configparser.ParsingError: 74 LOG.exception( 75 "There was an error trying to parse a config " 76 "file. The file with a problem was %s.", 77 filename, 78 ) 79 return (config, found_files) 80 81 def cli_config(self, files: str) -> configparser.RawConfigParser: 82 """Read and parse the config file specified on the command-line.""" 83 config, found_files = self._read_config(files) 84 if found_files: 85 LOG.debug("Found cli configuration files: %s", found_files) 86 return config 87 88 def generate_possible_local_files(self): 89 """Find and generate all local config files.""" 90 parent = tail = os.getcwd() 91 found_config_files = False 92 while tail and not found_config_files: 93 for project_filename in self.project_filenames: 94 filename = os.path.abspath( 95 os.path.join(parent, project_filename) 96 ) 97 if os.path.exists(filename): 98 yield filename 99 found_config_files = True 100 self.local_directory = parent 101 (parent, tail) = os.path.split(parent) 102 103 def local_config_files(self): 104 """Find all local config files which actually exist. 105 106 Filter results from 107 :meth:`~ConfigFileFinder.generate_possible_local_files` based 108 on whether the filename exists or not. 109 110 :returns: 111 List of files that exist that are local project config files with 112 extra config files appended to that list (which also exist). 113 :rtype: 114 [str] 115 """ 116 exists = os.path.exists 117 return [ 118 filename for filename in self.generate_possible_local_files() 119 ] + [f for f in self.extra_config_files if exists(f)] 120 121 def local_configs_with_files(self): 122 """Parse all local config files into one config object. 123 124 Return (config, found_config_files) tuple. 125 """ 126 config, found_files = self._read_config(*self.local_config_files()) 127 if found_files: 128 LOG.debug("Found local configuration files: %s", found_files) 129 return (config, found_files) 130 131 def local_configs(self): 132 """Parse all local config files into one config object.""" 133 return self.local_configs_with_files()[0] 134 135 136class ConfigParser: 137 """Encapsulate merging different types of configuration files. 138 139 This parses out the options registered that were specified in the 140 configuration files, handles extra configuration files, and returns 141 dictionaries with the parsed values. 142 """ 143 144 #: Set of actions that should use the 145 #: :meth:`~configparser.RawConfigParser.getbool` method. 146 GETBOOL_ACTIONS = {"store_true", "store_false"} 147 148 def __init__(self, option_manager, config_finder): 149 """Initialize the ConfigParser instance. 150 151 :param flake8.options.manager.OptionManager option_manager: 152 Initialized OptionManager. 153 :param flake8.options.config.ConfigFileFinder config_finder: 154 Initialized ConfigFileFinder. 155 """ 156 #: Our instance of flake8.options.manager.OptionManager 157 self.option_manager = option_manager 158 #: The prog value for the cli parser 159 self.program_name = option_manager.program_name 160 #: Mapping of configuration option names to 161 #: :class:`~flake8.options.manager.Option` instances 162 self.config_options = option_manager.config_options_dict 163 #: Our instance of our :class:`~ConfigFileFinder` 164 self.config_finder = config_finder 165 166 def _normalize_value(self, option, value, parent=None): 167 if parent is None: 168 parent = self.config_finder.local_directory 169 170 final_value = option.normalize(value, parent) 171 LOG.debug( 172 '%r has been normalized to %r for option "%s"', 173 value, 174 final_value, 175 option.config_name, 176 ) 177 return final_value 178 179 def _parse_config(self, config_parser, parent=None): 180 config_dict = {} 181 for option_name in config_parser.options(self.program_name): 182 if option_name not in self.config_options: 183 LOG.debug( 184 'Option "%s" is not registered. Ignoring.', option_name 185 ) 186 continue 187 option = self.config_options[option_name] 188 189 # Use the appropriate method to parse the config value 190 method = config_parser.get 191 if option.type is int or option.action == "count": 192 method = config_parser.getint 193 elif option.action in self.GETBOOL_ACTIONS: 194 method = config_parser.getboolean 195 196 value = method(self.program_name, option_name) 197 LOG.debug('Option "%s" returned value: %r', option_name, value) 198 199 final_value = self._normalize_value(option, value, parent) 200 config_dict[option.config_name] = final_value 201 202 return config_dict 203 204 def is_configured_by(self, config): 205 """Check if the specified config parser has an appropriate section.""" 206 return config.has_section(self.program_name) 207 208 def parse_local_config(self): 209 """Parse and return the local configuration files.""" 210 config = self.config_finder.local_configs() 211 if not self.is_configured_by(config): 212 LOG.debug( 213 "Local configuration files have no %s section", 214 self.program_name, 215 ) 216 return {} 217 218 LOG.debug("Parsing local configuration files.") 219 return self._parse_config(config) 220 221 def parse_cli_config(self, config_path): 222 """Parse and return the file specified by --config.""" 223 config = self.config_finder.cli_config(config_path) 224 if not self.is_configured_by(config): 225 LOG.debug( 226 "CLI configuration files have no %s section", 227 self.program_name, 228 ) 229 return {} 230 231 LOG.debug("Parsing CLI configuration files.") 232 return self._parse_config(config, os.path.dirname(config_path)) 233 234 def parse(self): 235 """Parse and return the local config files. 236 237 :returns: 238 Dictionary of parsed configuration options 239 :rtype: 240 dict 241 """ 242 if self.config_finder.ignore_config_files: 243 LOG.debug( 244 "Refusing to parse configuration files due to user-" 245 "requested isolation" 246 ) 247 return {} 248 249 if self.config_finder.config_file: 250 LOG.debug( 251 "Ignoring user and locally found configuration files. " 252 'Reading only configuration from "%s" specified via ' 253 "--config by the user", 254 self.config_finder.config_file, 255 ) 256 return self.parse_cli_config(self.config_finder.config_file) 257 258 return self.parse_local_config() 259 260 261def get_local_plugins(config_finder): 262 """Get local plugins lists from config files. 263 264 :param flake8.options.config.ConfigFileFinder config_finder: 265 The config file finder to use. 266 :returns: 267 LocalPlugins namedtuple containing two lists of plugin strings, 268 one for extension (checker) plugins and one for report plugins. 269 :rtype: 270 flake8.options.config.LocalPlugins 271 """ 272 local_plugins = LocalPlugins(extension=[], report=[], paths=[]) 273 if config_finder.ignore_config_files: 274 LOG.debug( 275 "Refusing to look for local plugins in configuration" 276 "files due to user-requested isolation" 277 ) 278 return local_plugins 279 280 if config_finder.config_file: 281 LOG.debug( 282 'Reading local plugins only from "%s" specified via ' 283 "--config by the user", 284 config_finder.config_file, 285 ) 286 config = config_finder.cli_config(config_finder.config_file) 287 config_files = [config_finder.config_file] 288 else: 289 config, config_files = config_finder.local_configs_with_files() 290 291 base_dirs = {os.path.dirname(cf) for cf in config_files} 292 293 section = f"{config_finder.program_name}:local-plugins" 294 for plugin_type in ["extension", "report"]: 295 if config.has_option(section, plugin_type): 296 local_plugins_string = config.get(section, plugin_type).strip() 297 plugin_type_list = getattr(local_plugins, plugin_type) 298 plugin_type_list.extend( 299 utils.parse_comma_separated_list( 300 local_plugins_string, regexp=utils.LOCAL_PLUGIN_LIST_RE 301 ) 302 ) 303 if config.has_option(section, "paths"): 304 raw_paths = utils.parse_comma_separated_list( 305 config.get(section, "paths").strip() 306 ) 307 norm_paths: List[str] = [] 308 for base_dir in base_dirs: 309 norm_paths.extend( 310 path 311 for path in utils.normalize_paths(raw_paths, parent=base_dir) 312 if os.path.exists(path) 313 ) 314 local_plugins.paths.extend(norm_paths) 315 return local_plugins 316 317 318LocalPlugins = collections.namedtuple("LocalPlugins", "extension report paths") 319