Skip to content

Commit 989626a

Browse files
syastrovmsullivan
authored andcommitted
Prevent dmypy from restarting if the configuration contains globs. (python#7960)
If a configuration contains globs, then dmypy incorrectly detected that the configration changed even though it did not. This caused the daemon to always restart if a configuration contained globs. Fixes python#7576.
1 parent 4c77d16 commit 989626a

File tree

3 files changed

+49
-14
lines changed

3 files changed

+49
-14
lines changed

mypy/options.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class Options:
6262

6363
def __init__(self) -> None:
6464
# Cache for clone_for_module()
65-
self.per_module_cache = None # type: Optional[Dict[str, Options]]
65+
self._per_module_cache = None # type: Optional[Dict[str, Options]]
6666

6767
# -- build options --
6868
self.build_type = BuildType.STANDARD
@@ -222,7 +222,7 @@ def __init__(self) -> None:
222222

223223
# Per-module options (raw)
224224
self.per_module_options = OrderedDict() # type: OrderedDict[str, Dict[str, object]]
225-
self.glob_options = [] # type: List[Tuple[str, Pattern[str]]]
225+
self._glob_options = [] # type: List[Tuple[str, Pattern[str]]]
226226
self.unused_configs = set() # type: Set[str]
227227

228228
# -- development options --
@@ -280,7 +280,8 @@ def snapshot(self) -> object:
280280
for k in get_class_descriptors(Options):
281281
if hasattr(self, k) and k != "new_semantic_analyzer":
282282
d[k] = getattr(self, k)
283-
del d['per_module_cache']
283+
# Remove private attributes from snapshot
284+
d = {k: v for k, v in d.items() if not k.startswith('_')}
284285
return d
285286

286287
def __repr__(self) -> str:
@@ -295,7 +296,7 @@ def apply_changes(self, changes: Dict[str, object]) -> 'Options':
295296
return new_options
296297

297298
def build_per_module_cache(self) -> None:
298-
self.per_module_cache = {}
299+
self._per_module_cache = {}
299300

300301
# Config precedence is as follows:
301302
# 1. Concrete section names: foo.bar.baz
@@ -320,7 +321,7 @@ def build_per_module_cache(self) -> None:
320321
concrete = [k for k in structured_keys if not k.endswith('.*')]
321322

322323
for glob in unstructured_glob_keys:
323-
self.glob_options.append((glob, self.compile_glob(glob)))
324+
self._glob_options.append((glob, self.compile_glob(glob)))
324325

325326
# We (for ease of implementation) treat unstructured glob
326327
# sections as used if any real modules use them or if any
@@ -333,7 +334,7 @@ def build_per_module_cache(self) -> None:
333334
# on inheriting from parent configs.
334335
options = self.clone_for_module(key)
335336
# And then update it with its per-module options.
336-
self.per_module_cache[key] = options.apply_changes(self.per_module_options[key])
337+
self._per_module_cache[key] = options.apply_changes(self.per_module_options[key])
337338

338339
# Add the more structured sections into unused configs, since
339340
# they only count as used if actually used by a real module.
@@ -345,14 +346,14 @@ def clone_for_module(self, module: str) -> 'Options':
345346
NOTE: Once this method is called all Options objects should be
346347
considered read-only, else the caching might be incorrect.
347348
"""
348-
if self.per_module_cache is None:
349+
if self._per_module_cache is None:
349350
self.build_per_module_cache()
350-
assert self.per_module_cache is not None
351+
assert self._per_module_cache is not None
351352

352353
# If the module just directly has a config entry, use it.
353-
if module in self.per_module_cache:
354+
if module in self._per_module_cache:
354355
self.unused_configs.discard(module)
355-
return self.per_module_cache[module]
356+
return self._per_module_cache[module]
356357

357358
# If not, search for glob paths at all the parents. So if we are looking for
358359
# options for foo.bar.baz, we search foo.bar.baz.*, foo.bar.*, foo.*,
@@ -363,15 +364,15 @@ def clone_for_module(self, module: str) -> 'Options':
363364
path = module.split('.')
364365
for i in range(len(path), 0, -1):
365366
key = '.'.join(path[:i] + ['*'])
366-
if key in self.per_module_cache:
367+
if key in self._per_module_cache:
367368
self.unused_configs.discard(key)
368-
options = self.per_module_cache[key]
369+
options = self._per_module_cache[key]
369370
break
370371

371372
# OK and *now* we need to look for unstructured glob matches.
372373
# We only do this for concrete modules, not structured wildcards.
373374
if not module.endswith('.*'):
374-
for key, pattern in self.glob_options:
375+
for key, pattern in self._glob_options:
375376
if pattern.match(module):
376377
self.unused_configs.discard(key)
377378
options = options.apply_changes(self.per_module_options[key])

mypy/test/testdaemon.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from mypy.test.config import test_temp_dir, PREFIX
1313
from mypy.test.data import DataDrivenTestCase, DataSuite
14-
from mypy.test.helpers import assert_string_arrays_equal
14+
from mypy.test.helpers import assert_string_arrays_equal, normalize_error_messages
1515

1616
# Files containing test cases descriptions.
1717
daemon_files = [
@@ -40,6 +40,7 @@ def test_daemon(testcase: DataDrivenTestCase) -> None:
4040
cmd = cmd.replace('{python}', sys.executable)
4141
sts, output = run_cmd(cmd)
4242
output_lines = output.splitlines()
43+
output_lines = normalize_error_messages(output_lines)
4344
if sts:
4445
output_lines.append('== Return code: %d' % sts)
4546
assert_string_arrays_equal(expected_lines,

test-data/unit/daemon.test

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,39 @@ from mypy.plugin import Plugin
9999
class Dummy(Plugin): pass
100100
def plugin(version): return Dummy
101101

102+
[case testDaemonRunRestartGlobs]
103+
-- Ensure dmypy is not restarted if the configuration doesn't change and it contains globs
104+
-- Note: Backslash path separator in output is replaced with forward slash so the same test succeeds on Windows as well
105+
$ dmypy run -- foo --follow-imports=error --python-version=3.6
106+
Daemon started
107+
foo/lol.py:1: error: Name 'fail' is not defined
108+
Found 1 error in 1 file (checked 3 source files)
109+
== Return code: 1
110+
$ dmypy run -- foo --follow-imports=error --python-version=3.6
111+
foo/lol.py:1: error: Name 'fail' is not defined
112+
Found 1 error in 1 file (checked 3 source files)
113+
== Return code: 1
114+
$ {python} -c "print('[mypy]')" >mypy.ini
115+
$ {python} -c "print('ignore_errors=True')" >>mypy.ini
116+
$ dmypy run -- foo --follow-imports=error --python-version=3.6
117+
Restarting: configuration changed
118+
Daemon stopped
119+
Daemon started
120+
Success: no issues found in 3 source files
121+
$ dmypy stop
122+
Daemon stopped
123+
[file mypy.ini]
124+
\[mypy]
125+
ignore_errors = True
126+
\[mypy-*.lol]
127+
ignore_errors = False
128+
129+
[file foo/__init__.py]
130+
[file foo/lol.py]
131+
fail
132+
[file foo/ok.py]
133+
a: int = 1
134+
102135
[case testDaemonStatusKillRestartRecheck]
103136
$ dmypy status
104137
No status file found

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy