Skip to content

Commit c29210e

Browse files
authored
Allow wildcards inside of configuration section names (#5120)
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 f61c2ba commit c29210e

File tree

4 files changed

+133
-25
lines changed

4 files changed

+133
-25
lines changed

docs/source/config_file.rst

+23-3
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,35 @@ 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 stars may
45+
appear in the middle of a name (e.g
46+
``site.*.migrations.*``). Stars match zero or more module
47+
components (so ``site.*.migrations.*`` can match ``site.migrations``).
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+
59+
The difference in precedence order between "structured" patterns (by
60+
specificity) and "unstructured" patterns (by order in the file) is
61+
unfortunate, and is subject to change in future versions.
62+
4363
.. note::
4464

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

mypy/main.py

+3-2
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

+66-16
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from collections import OrderedDict
2+
import re
23
import pprint
34
import sys
45

5-
from typing import Dict, List, Mapping, MutableMapping, Optional, Set, Tuple
6+
from typing import Dict, List, Mapping, MutableMapping, Optional, Pattern, Set, Tuple
67

78
from mypy import defaults
89

@@ -167,8 +168,8 @@ def __init__(self) -> None:
167168
self.plugins = [] # type: List[str]
168169

169170
# Per-module options (raw)
170-
pm_opts = OrderedDict() # type: OrderedDict[str, Dict[str, object]]
171-
self.per_module_options = pm_opts
171+
self.per_module_options = OrderedDict() # type: OrderedDict[str, Dict[str, object]]
172+
self.glob_options = [] # type: List[Tuple[str, Pattern[str]]]
172173
self.unused_configs = set() # type: Set[str]
173174

174175
# -- development options --
@@ -208,27 +209,56 @@ def __ne__(self, other: object) -> bool:
208209
def __repr__(self) -> str:
209210
return 'Options({})'.format(pprint.pformat(self.snapshot()))
210211

212+
def apply_changes(self, changes: Dict[str, object]) -> 'Options':
213+
new_options = Options()
214+
new_options.__dict__.update(self.__dict__)
215+
new_options.__dict__.update(changes)
216+
return new_options
217+
211218
def build_per_module_cache(self) -> None:
212219
self.per_module_cache = {}
213-
# Since configs inherit from glob configs above them in the hierarchy,
220+
221+
# Config precedence is as follows:
222+
# 1. Concrete section names: foo.bar.baz
223+
# 2. "Unstructured" glob patterns: foo.*.baz, in the order
224+
# they appear in the file (last wins)
225+
# 3. "Well-structured" wildcard patterns: foo.bar.*, in specificity order.
226+
227+
# Since structured configs inherit from structured 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+
# (A section being "processed last" results in its config "winning".)
235+
# Unstructured glob configs are stored and are all checked for each module.
236+
unstructured_glob_keys = [k for k in self.per_module_options.keys()
237+
if '*' in k[:-1]]
238+
structured_keys = [k for k in self.per_module_options.keys()
239+
if '*' not in k[:-1]]
240+
wildcards = sorted(k for k in structured_keys if k.endswith('.*'))
241+
concrete = [k for k in structured_keys if not k.endswith('.*')]
242+
243+
for glob in unstructured_glob_keys:
244+
self.glob_options.append((glob, self.compile_glob(glob)))
245+
246+
# We (for ease of implementation) treat unstructured glob
247+
# sections as used if any real modules use them or if any
248+
# concrete config sections use them. This means we need to
249+
# track which get used while constructing.
250+
self.unused_configs = set(unstructured_glob_keys)
251+
252+
for key in wildcards + concrete:
222253
# Find what the options for this key would be, just based
223254
# on inheriting from parent configs.
224255
options = self.clone_for_module(key)
225256
# 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])
229-
self.per_module_cache[key] = new_options
257+
self.per_module_cache[key] = options.apply_changes(self.per_module_options[key])
230258

231-
self.unused_configs = set(keys)
259+
# Add the more structured sections into unused configs, since
260+
# they only count as used if actually used by a real module.
261+
self.unused_configs.update(structured_keys)
232262

233263
def clone_for_module(self, module: str) -> 'Options':
234264
"""Create an Options object that incorporates per-module options.
@@ -250,18 +280,38 @@ def clone_for_module(self, module: str) -> 'Options':
250280
# in that order, looking for an entry.
251281
# This is technically quadratic in the length of the path, but module paths
252282
# don't actually get all that long.
283+
options = self
253284
path = module.split('.')
254285
for i in range(len(path), 0, -1):
255286
key = '.'.join(path[:i] + ['*'])
256287
if key in self.per_module_cache:
257288
self.unused_configs.discard(key)
258-
return self.per_module_cache[key]
289+
options = self.per_module_cache[key]
290+
break
291+
292+
# OK and *now* we need to look for unstructured glob matches.
293+
# We only do this for concrete modules, not structured wildcards.
294+
if not module.endswith('.*'):
295+
for key, pattern in self.glob_options:
296+
if pattern.match(module):
297+
self.unused_configs.discard(key)
298+
options = options.apply_changes(self.per_module_options[key])
259299

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

264-
return self
304+
return options
305+
306+
def compile_glob(self, s: str) -> Pattern[str]:
307+
# Compile one of the glob patterns to a regex so that '.*' can
308+
# match *zero or more* module sections. This means we compile
309+
# '.*' into '(\..*)?'.
310+
parts = s.split('.')
311+
expr = re.escape(parts[0]) if parts[0] != '*' else '.*'
312+
for part in parts[1:]:
313+
expr += re.escape('.' + part) if part != '*' else '(\..*)?'
314+
return re.compile(expr + '\\Z')
265315

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

test-data/unit/cmdline.test

+41-4
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,6 @@ 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.*'
272271
== Return code: 0
273272

274273
[case testConfigMypyPath]
@@ -1179,10 +1178,48 @@ warn_unused_configs = True
11791178
[[mypy-spam.eggs]
11801179
[[mypy-emarg.*]
11811180
[[mypy-emarg.hatch]
1181+
-- Currently we don't treat an unstructured pattern like a.*.b as unused
1182+
-- if it matches another section (like a.x.b). This would be reasonable
1183+
-- to change.
1184+
[[mypy-a.*.b]
1185+
[[mypy-a.*.c]
1186+
[[mypy-a.x.b]
11821187
[file foo.py]
11831188
[file quux.py]
11841189
[file spam/__init__.py]
11851190
[file spam/eggs.py]
11861191
[out]
1187-
Warning: unused section(s) in mypy.ini: [mypy-bar], [mypy-baz.*], [mypy-emarg.*], [mypy-emarg.hatch]
1192+
Warning: unused section(s) in mypy.ini: [mypy-bar], [mypy-baz.*], [mypy-emarg.*], [mypy-emarg.hatch], [mypy-a.*.c], [mypy-a.x.b]
11881193
== Return code: 0
1194+
1195+
[case testConfigUnstructuredGlob]
1196+
# cmd: mypy emarg foo
1197+
[file mypy.ini]
1198+
[[mypy]
1199+
ignore_errors = true
1200+
[[mypy-*.lol]
1201+
ignore_errors = false
1202+
[[mypy-emarg.*]
1203+
ignore_errors = false
1204+
[[mypy-emarg.*.villip.*]
1205+
ignore_errors = true
1206+
[[mypy-emarg.hatch.villip.mankangulisk]
1207+
ignore_errors = false
1208+
[file emarg/__init__.py]
1209+
[file emarg/foo.py]
1210+
fail
1211+
[file emarg/villip.py]
1212+
fail
1213+
[file emarg/hatch/__init__.py]
1214+
[file emarg/hatch/villip/__init__.py]
1215+
[file emarg/hatch/villip/nus.py]
1216+
fail
1217+
[file emarg/hatch/villip/mankangulisk.py]
1218+
fail
1219+
[file foo/__init__.py]
1220+
[file foo/lol.py]
1221+
fail
1222+
[out]
1223+
foo/lol.py:1: error: Name 'fail' is not defined
1224+
emarg/foo.py:1: error: Name 'fail' is not defined
1225+
emarg/hatch/villip/mankangulisk.py:1: error: Name 'fail' is not defined

0 commit comments

Comments
 (0)