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