Skip to content

Commit fcda3f8

Browse files
committed
Allow wildcards inside of configuration section names
This is to support Django-style usecases with patterns like `site.*.migrations.*`. The implementation works by mixing in the old-style glob matching with the new structured matching. Fixes #5014.
1 parent 0447473 commit fcda3f8

File tree

4 files changed

+112
-24
lines changed

4 files changed

+112
-24
lines changed

docs/source/config_file.rst

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,31 @@ characters.
3131

3232
- Additional sections named ``[mypy-PATTERN1,PATTERN2,...]`` may be
3333
present, where ``PATTERN1``, ``PATTERN2``, etc., are comma-separated
34-
patterns of the form ``dotted_module_name`` or ``dotted_module_name.*``.
34+
patterns of fully-qualified module names, with some components optionally
35+
replaced by `*`s (e.g. ``foo.bar``, ``foo.bar.*``, ``foo.*.baz``).
3536
These sections specify additional flags that only apply to *modules*
3637
whose name matches at least one of the patterns.
3738

38-
A pattern of the form ``dotted_module_name`` matches only the named module,
39-
while ``dotted_module_name.*`` matches ``dotted_module_name`` and any
39+
A pattern of the form ``qualified_module_name`` matches only the named module,
40+
while ``qualified_module_name.*`` matches ``dotted_module_name`` and any
4041
submodules (so ``foo.bar.*`` would match all of ``foo.bar``,
4142
``foo.bar.baz``, and ``foo.bar.baz.quux``).
4243

44+
Patterns may also be "unstructured" wildcards, in which ``*``s may
45+
appear in the middle of a name (e.g
46+
``site.*.migrations.*``). Internal ``*``s match one or more module
47+
component.
48+
49+
When options conflict, the precedence order for the configuration sections is:
50+
1. Sections with concrete module names (``foo.bar``)
51+
2. Sections with "unstructured" wildcard patterns (``foo.*.baz``),
52+
with sections later in the configuration file overriding
53+
sections earlier.
54+
3. Sections with "well-structured" wildcard patterns
55+
(``foo.bar.*``), with more specific overriding more general.
56+
4. Command line options.
57+
5. Top-level configuration file options.
58+
4359
.. note::
4460

4561
The ``warn_unused_configs`` flag may be useful to debug misspelled

mypy/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -746,8 +746,9 @@ def parse_config_file(options: Options, filename: Optional[str]) -> None:
746746
glob = glob.replace(os.altsep, '.')
747747

748748
if (any(c in glob for c in '?[]!') or
749-
('*' in glob and (not glob.endswith('.*') or '*' in glob[:-2]))):
750-
print("%s: Invalid pattern. Patterns must be 'module_name' or 'module_name.*'"
749+
any('*' in x and x != '*' for x in glob.split('.'))):
750+
print("%s: Patterns must be fully-qualified module names, optionally "
751+
"with '*' in some components (e.g spam.*.eggs.*)'"
751752
% prefix,
752753
file=sys.stderr)
753754
else:

mypy/options.py

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from collections import OrderedDict
2+
import fnmatch
3+
import re
24
import pprint
35
import sys
46

5-
from typing import Dict, List, Mapping, MutableMapping, Optional, Set, Tuple
7+
from typing import Dict, List, Mapping, MutableMapping, Optional, Pattern, Set, Tuple
68

79
from mypy import defaults
810

@@ -167,8 +169,8 @@ def __init__(self) -> None:
167169
self.plugins = [] # type: List[str]
168170

169171
# 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]]]
172174
self.unused_configs = set() # type: Set[str]
173175

174176
# -- development options --
@@ -208,27 +210,54 @@ def __ne__(self, other: object) -> bool:
208210
def __repr__(self) -> str:
209211
return 'Options({})'.format(pprint.pformat(self.snapshot()))
210212

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+
211219
def build_per_module_cache(self) -> None:
212220
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,
214228
# 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
217232
# exploit the fact that foo.* sorts earlier ASCIIbetically (unicodebetically?)
218233
# 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:
222252
# Find what the options for this key would be, just based
223253
# on inheriting from parent configs.
224254
options = self.clone_for_module(key)
225255
# 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])
229257
self.per_module_cache[key] = new_options
230258

231-
self.unused_configs = set(keys)
259+
# Add the more structured sections into unused configs .
260+
self.unused_configs.update(structured_keys)
232261

233262
def clone_for_module(self, module: str) -> 'Options':
234263
"""Create an Options object that incorporates per-module options.
@@ -250,18 +279,33 @@ def clone_for_module(self, module: str) -> 'Options':
250279
# in that order, looking for an entry.
251280
# This is technically quadratic in the length of the path, but module paths
252281
# don't actually get all that long.
282+
options = self
253283
path = module.split('.')
254284
for i in range(len(path), 0, -1):
255285
key = '.'.join(path[:i] + ['*'])
256286
if key in self.per_module_cache:
257287
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])
259297

260298
# We could update the cache to directly point to modules once
261299
# they have been looked up, but in testing this made things
262300
# slower and not faster, so we don't bother.
263301

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
265309

266310
def select_options_affecting_cache(self) -> Mapping[str, object]:
267311
return {opt: getattr(self, opt) for opt in self.OPTIONS_AFFECTING_CACHE}

test-data/unit/cmdline.test

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,8 @@ def g(a: int) -> int: return f(a)
203203
def f(a): pass
204204
def g(a: int) -> int: return f(a)
205205
[out]
206-
mypy.ini: [mypy-*x*]: Invalid pattern. Patterns must be 'module_name' or 'module_name.*'
207-
mypy.ini: [mypy-*y*]: Invalid pattern. Patterns must be 'module_name' or 'module_name.*'
206+
mypy.ini: [mypy-*x*]: Patterns must be fully-qualified module names, optionally with '*' in some components (e.g spam.*.eggs.*)
207+
mypy.ini: [mypy-*y*]: Patterns must be fully-qualified module names, optionally with '*' in some components (e.g spam.*.eggs.*)
208208
== Return code: 0
209209

210210
[case testMultipleGlobConfigSection]
@@ -268,7 +268,7 @@ mypy.ini: [mypy]: ignore_missing_imports: Not a boolean: nah
268268
python_version = 3.4
269269
[out]
270270
mypy.ini: [mypy-*]: Per-module sections should only specify per-module flags (python_version)
271-
mypy.ini: [mypy-*]: Invalid pattern. Patterns must be 'module_name' or 'module_name.*'
271+
mypy.ini: [mypy-*]: Patterns must be fully-qualified module names, optionally with '*' in some components (e.g spam.*.eggs.*)
272272
== Return code: 0
273273

274274
[case testConfigMypyPath]
@@ -1179,10 +1179,37 @@ warn_unused_configs = True
11791179
[[mypy-spam.eggs]
11801180
[[mypy-emarg.*]
11811181
[[mypy-emarg.hatch]
1182+
[[mypy-a.*.b]
1183+
[[mypy-a.*.c]
1184+
[[mypy-a.x.b]
11821185
[file foo.py]
11831186
[file quux.py]
11841187
[file spam/__init__.py]
11851188
[file spam/eggs.py]
11861189
[out]
1187-
Warning: unused section(s) in mypy.ini: [mypy-bar], [mypy-baz.*], [mypy-emarg.*], [mypy-emarg.hatch]
1190+
Warning: unused section(s) in mypy.ini: [mypy-bar], [mypy-baz.*], [mypy-emarg.*], [mypy-emarg.hatch], [mypy-a.*.c], [mypy-a.x.b]
11881191
== Return code: 0
1192+
1193+
[case testConfigNonsense]
1194+
# cmd: mypy emarg
1195+
[file mypy.ini]
1196+
[[mypy]
1197+
ignore_errors = true
1198+
[[mypy-emarg.*]
1199+
ignore_errors = false
1200+
[[mypy-emarg.*.vilip.*]
1201+
ignore_errors = true
1202+
[[mypy-emarg.hatch.vilip.mankangulisk]
1203+
ignore_errors = false
1204+
[file emarg/__init__.py]
1205+
[file emarg/foo.py]
1206+
fail
1207+
[file emarg/hatch/__init__.py]
1208+
[file emarg/hatch/vilip/__init__.py]
1209+
[file emarg/hatch/vilip/nus.py]
1210+
fail
1211+
[file emarg/hatch/vilip/mankangulisk.py]
1212+
fail
1213+
[out]
1214+
emarg/foo.py:1: error: Name 'fail' is not defined
1215+
emarg/hatch/vilip/mankangulisk.py:1: error: Name 'fail' is not defined

0 commit comments

Comments
 (0)