Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 47b928d

Browse files
authored
Add a plugin hook for specifying per-module configuration data (python#7871)
This is useful in general for plugins that might have per-module configuration and we will be able to use it to manage the interaction between mypyc caches and mypy caches without needing to add custom mypyc hooks.
1 parent 8d44536 commit 47b928d

6 files changed

Lines changed: 80 additions & 0 deletions

File tree

docs/source/extending_mypy.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,14 @@ module. It is called before semantic analysis. For example, this can
236236
be used if a library has dependencies that are dynamically loaded
237237
based on configuration information.
238238

239+
**report_config_data()** can be used if the plugin has some sort of
240+
per-module configuration that can affect typechecking. In that case,
241+
when the configuration for a module changes, we want to invalidate
242+
mypy's cache for that module so that it can be rechecked. This hook
243+
should be used to report to mypy any relevant configuration data,
244+
so that mypy knows to recheck the module if the configuration changes.
245+
The hooks hould return data encodable as JSON.
246+
239247
Notes about the semantic analyzer
240248
*********************************
241249

mypy/build.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ def default_data_dir() -> str:
269269
('interface_hash', str), # hash representing the public interface
270270
('version_id', str), # mypy version for cache invalidation
271271
('ignore_all', bool), # if errors were ignored
272+
('plugin_data', Any), # config data from plugins
272273
])
273274
# NOTE: dependencies + suppressed == all reachable imports;
274275
# suppressed contains those reachable imports that were prevented by
@@ -303,6 +304,7 @@ def cache_meta_from_dict(meta: Dict[str, Any], data_json: str) -> CacheMeta:
303304
meta.get('interface_hash', ''),
304305
meta.get('version_id', sentinel),
305306
meta.get('ignore_all', True),
307+
meta.get('plugin_data', None),
306308
)
307309

308310

@@ -1162,6 +1164,13 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache
11621164
if manager.plugins_snapshot != manager.old_plugins_snapshot:
11631165
manager.log('Metadata abandoned for {}: plugins differ'.format(id))
11641166
return None
1167+
# So that plugins can return data with tuples in it without
1168+
# things silently always invalidating modules, we round-trip
1169+
# the config data. This isn't beautiful.
1170+
plugin_data = json.loads(json.dumps(manager.plugin.report_config_data(id, path)))
1171+
if m.plugin_data != plugin_data:
1172+
manager.log('Metadata abandoned for {}: plugin configuration differs'.format(id))
1173+
return None
11651174

11661175
manager.add_stats(fresh_metas=1)
11671176
return m
@@ -1284,6 +1293,7 @@ def validate_meta(meta: Optional[CacheMeta], id: str, path: Optional[str],
12841293
'interface_hash': meta.interface_hash,
12851294
'version_id': manager.version_id,
12861295
'ignore_all': meta.ignore_all,
1296+
'plugin_data': meta.plugin_data,
12871297
}
12881298
if manager.options.debug_cache:
12891299
meta_str = json.dumps(meta_dict, indent=2, sort_keys=True)
@@ -1366,6 +1376,8 @@ def write_cache(id: str, path: str, tree: MypyFile,
13661376
data_str = json_dumps(data, manager.options.debug_cache)
13671377
interface_hash = compute_hash(data_str)
13681378

1379+
plugin_data = manager.plugin.report_config_data(id, path)
1380+
13691381
# Obtain and set up metadata
13701382
try:
13711383
st = manager.get_stat(path)
@@ -1429,6 +1441,7 @@ def write_cache(id: str, path: str, tree: MypyFile,
14291441
'interface_hash': interface_hash,
14301442
'version_id': manager.version_id,
14311443
'ignore_all': ignore_all,
1444+
'plugin_data': plugin_data,
14321445
}
14331446

14341447
# Write meta cache file

mypy/interpreted_plugin.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ def lookup_fully_qualified(self, fullname: str) -> Optional[SymbolTableNode]:
4141
assert self._modules is not None
4242
return lookup_fully_qualified(fullname, self._modules)
4343

44+
def report_config_data(self, id: str, path: str) -> Any:
45+
return None
46+
4447
def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]:
4548
return []
4649

mypy/plugin.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,19 @@ def lookup_fully_qualified(self, fullname: str) -> Optional[SymbolTableNode]:
459459
assert self._modules is not None
460460
return lookup_fully_qualified(fullname, self._modules)
461461

462+
def report_config_data(self, id: str, path: str) -> Any:
463+
"""Get representation of configuration data for a module.
464+
465+
The data must be encodable as JSON and will be stored in the
466+
cache metadata for the module. A mismatch between the cached
467+
values and the returned will result in that module's cache
468+
being invalidated and the module being rechecked.
469+
470+
This can be used to incorporate external configuration information
471+
that might require changes to typechecking.
472+
"""
473+
return None
474+
462475
def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]:
463476
"""Customize dependencies for a module.
464477
@@ -666,6 +679,9 @@ def set_modules(self, modules: Dict[str, MypyFile]) -> None:
666679
def lookup_fully_qualified(self, fullname: str) -> Optional[SymbolTableNode]:
667680
return self.plugin.lookup_fully_qualified(fullname)
668681

682+
def report_config_data(self, id: str, path: str) -> Any:
683+
return self.plugin.report_config_data(id, path)
684+
669685
def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]:
670686
return self.plugin.get_additional_deps(file)
671687

@@ -734,6 +750,10 @@ def set_modules(self, modules: Dict[str, MypyFile]) -> None:
734750
for plugin in self._plugins:
735751
plugin.set_modules(modules)
736752

753+
def report_config_data(self, id: str, path: str) -> Any:
754+
config_data = [plugin.report_config_data(id, path) for plugin in self._plugins]
755+
return config_data if any(x is not None for x in config_data) else None
756+
737757
def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]:
738758
deps = []
739759
for plugin in self._plugins:

test-data/unit/check-incremental.test

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4936,6 +4936,24 @@ python_version=3.6
49364936
[out2]
49374937
tmp/a.py:2: error: Incompatible types in assignment (expression has type "int", variable has type "str")
49384938

4939+
[case testPluginConfigData]
4940+
# flags: --config-file tmp/mypy.ini
4941+
import a
4942+
import b
4943+
[file a.py]
4944+
[file b.py]
4945+
[file test.json]
4946+
{"a": false, "b": false}
4947+
[file test.json.2]
4948+
{"a": true, "b": false}
4949+
4950+
[file mypy.ini]
4951+
\[mypy]
4952+
plugins=<ROOT>/test-data/unit/plugins/config_data.py
4953+
4954+
# The config change will force a to be rechecked but not b.
4955+
[rechecked a]
4956+
49394957
[case testLiteralIncrementalTurningIntoLiteral]
49404958
import mod
49414959
reveal_type(mod.a)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import os
2+
import json
3+
4+
from typing import Any
5+
6+
from mypy.plugin import Plugin
7+
8+
9+
class ConfigDataPlugin(Plugin):
10+
def report_config_data(self, id: str, path: str) -> Any:
11+
path = os.path.join('tmp/test.json')
12+
with open(path) as f:
13+
data = json.load(f)
14+
return data.get(id)
15+
16+
17+
def plugin(version):
18+
return ConfigDataPlugin

0 commit comments

Comments
 (0)