1
1
from collections import OrderedDict
2
+ import fnmatch
3
+ import re
2
4
import pprint
3
5
import sys
4
6
5
- from typing import Dict , List , Mapping , MutableMapping , Optional , Set , Tuple
7
+ from typing import Dict , List , Mapping , MutableMapping , Optional , Pattern , Set , Tuple
6
8
7
9
from mypy import defaults
8
10
@@ -167,8 +169,8 @@ def __init__(self) -> None:
167
169
self .plugins = [] # type: List[str]
168
170
169
171
# Per-module options (raw)
170
- pm_opts = OrderedDict () # type: OrderedDict[str, Dict[str, object]]
171
- self .per_module_options = pm_opts
172
+ self . per_module_options = OrderedDict () # type: OrderedDict[str, Dict[str, object]]
173
+ self .glob_options = [] # type: List[Tuple[str, Pattern[str]]]
172
174
self .unused_configs = set () # type: Set[str]
173
175
174
176
# -- development options --
@@ -208,27 +210,54 @@ def __ne__(self, other: object) -> bool:
208
210
def __repr__ (self ) -> str :
209
211
return 'Options({})' .format (pprint .pformat (self .snapshot ()))
210
212
213
+ def apply_changes (self , changes : Dict [str , object ]) -> 'Options' :
214
+ new_options = Options ()
215
+ new_options .__dict__ .update (self .__dict__ )
216
+ new_options .__dict__ .update (changes )
217
+ return new_options
218
+
211
219
def build_per_module_cache (self ) -> None :
212
220
self .per_module_cache = {}
213
- # Since configs inherit from glob configs above them in the hierarchy,
221
+
222
+ # Config precedence is as follows:
223
+ # 1. Concrete section names: foo.bar.baz
224
+ # 2. "Unstructured" glob patterns: foo.*.baz, in the order they appear in the file
225
+ # 3. "Well-structured" wildcard patterns: foo.bar.*, in specificity order.
226
+
227
+ # Since structured configs inherit from glob configs above them in the hierarchy,
214
228
# we need to process per-module configs in a careful order.
215
- # We have to process foo.* before foo.bar.* before foo.bar.
216
- # To do this, process all glob configs before non-glob configs and
229
+ # We have to process foo.* before foo.bar.* before foo.bar,
230
+ # and we need to apply *.bar to foo.bar but not to foo.bar.*.
231
+ # To do this, process all well-structured glob configs before non-glob configs and
217
232
# exploit the fact that foo.* sorts earlier ASCIIbetically (unicodebetically?)
218
233
# than foo.bar.*.
219
- keys = (sorted (k for k in self .per_module_options .keys () if k .endswith ('.*' )) +
220
- [k for k in self .per_module_options .keys () if not k .endswith ('.*' )])
221
- for key in keys :
234
+ # Unstructured glob configs are stored and are all checked for each module.
235
+ unstructured_glob_keys = [k for k in self .per_module_options .keys ()
236
+ if '*' in k [:- 1 ]]
237
+ structured_keys = [k for k in self .per_module_options .keys ()
238
+ if '*' not in k [:- 1 ]]
239
+ wildcards = sorted (k for k in structured_keys if k .endswith ('.*' ))
240
+ concrete = [k for k in structured_keys if not k .endswith ('.*' )]
241
+
242
+ for glob in unstructured_glob_keys :
243
+ self .glob_options .append ((glob , re .compile (fnmatch .translate (glob ))))
244
+
245
+ # We (for ease of implementation), treat unstructured glob
246
+ # sections as used if any real modules use them or if any
247
+ # concrete config sections use them. This means we need to
248
+ # track which get used while constructing.
249
+ self .unused_configs = set (unstructured_glob_keys )
250
+
251
+ for key in wildcards + concrete :
222
252
# Find what the options for this key would be, just based
223
253
# on inheriting from parent configs.
224
254
options = self .clone_for_module (key )
225
255
# And then update it with its per-module options.
226
- new_options = Options ()
227
- new_options .__dict__ .update (options .__dict__ )
228
- new_options .__dict__ .update (self .per_module_options [key ])
256
+ new_options = options .apply_changes (self .per_module_options [key ])
229
257
self .per_module_cache [key ] = new_options
230
258
231
- self .unused_configs = set (keys )
259
+ # Add the more structured sections into unused configs .
260
+ self .unused_configs .update (structured_keys )
232
261
233
262
def clone_for_module (self , module : str ) -> 'Options' :
234
263
"""Create an Options object that incorporates per-module options.
@@ -250,18 +279,33 @@ def clone_for_module(self, module: str) -> 'Options':
250
279
# in that order, looking for an entry.
251
280
# This is technically quadratic in the length of the path, but module paths
252
281
# don't actually get all that long.
282
+ options = self
253
283
path = module .split ('.' )
254
284
for i in range (len (path ), 0 , - 1 ):
255
285
key = '.' .join (path [:i ] + ['*' ])
256
286
if key in self .per_module_cache :
257
287
self .unused_configs .discard (key )
258
- return self .per_module_cache [key ]
288
+ options = self .per_module_cache [key ]
289
+ break
290
+
291
+ # OK and *now* we need to look for glob matches
292
+ if not module .endswith ('.*' ):
293
+ for key , pattern in self .glob_options :
294
+ if self .module_matches_pattern (module , pattern ):
295
+ self .unused_configs .discard (key )
296
+ options = options .apply_changes (self .per_module_options [key ])
259
297
260
298
# We could update the cache to directly point to modules once
261
299
# they have been looked up, but in testing this made things
262
300
# slower and not faster, so we don't bother.
263
301
264
- return self
302
+ return options
303
+
304
+ def module_matches_pattern (self , module : str , pattern : Pattern [str ]) -> bool :
305
+ # If the pattern is 'mod.*', we want 'mod' to match that too.
306
+ # (That's so that a pattern specifying a package also matches
307
+ # that package's __init__.)
308
+ return pattern .match (module ) is not None or pattern .match (module + '.' ) is not None
265
309
266
310
def select_options_affecting_cache (self ) -> Mapping [str , object ]:
267
311
return {opt : getattr (self , opt ) for opt in self .OPTIONS_AFFECTING_CACHE }
0 commit comments