Skip to content

Commit 89e1286

Browse files
committed
Add initial version of stubtest script developed by Jukka
(With some minor fixes.)
1 parent aec4595 commit 89e1286

File tree

2 files changed

+273
-0
lines changed

2 files changed

+273
-0
lines changed

scripts/dumpmodule.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""Dump the runtime structure of a module as JSON.
2+
3+
This is used for testing stubs.
4+
5+
This needs to run in Python 2.7 and 3.x.
6+
"""
7+
8+
from __future__ import print_function
9+
10+
import importlib
11+
import json
12+
import sys
13+
import types
14+
from typing import Text
15+
16+
17+
if sys.version_info >= (3, 0):
18+
import inspect
19+
long = int
20+
else:
21+
import inspect2 as inspect
22+
23+
24+
25+
def dump_module(id):
26+
m = importlib.import_module(id)
27+
data = module_to_json(m)
28+
print(json.dumps(data, ensure_ascii=True, indent=4, sort_keys=True))
29+
30+
31+
def module_to_json(m):
32+
result = {}
33+
for name, value in m.__dict__.items():
34+
# Filter out some useless attributes.
35+
if name in ('__file__',
36+
'__doc__',
37+
'__name__',
38+
'__builtins__',
39+
'__package__'):
40+
continue
41+
if name == '__all__':
42+
result[name] = sorted(value)
43+
else:
44+
result[name] = dump_value(value)
45+
return result
46+
47+
48+
def dump_value(value, depth=0):
49+
if depth > 10:
50+
return 'max_recursion_depth_exceeded'
51+
if isinstance(value, type):
52+
return dump_class(value, depth + 1)
53+
if inspect.isfunction(value):
54+
return dump_function(value)
55+
if callable(value):
56+
return 'callable' # TODO more information
57+
if isinstance(value, types.ModuleType):
58+
return 'module' # TODO module name
59+
if inspect.isdatadescriptor(value):
60+
return 'datadescriptor'
61+
62+
if inspect.ismemberdescriptor(value):
63+
return 'memberdescriptor'
64+
return dump_simple(value)
65+
66+
67+
def dump_simple(value):
68+
if type(value) in (int, bool, float, str, bytes, Text, long, list, set, dict, tuple):
69+
return type(value).__name__
70+
if value is None:
71+
return 'None'
72+
if value is inspect.Parameter.empty:
73+
return None
74+
return 'unknown'
75+
76+
77+
def dump_class(value, depth):
78+
return {
79+
'type': 'class',
80+
'attributes': dump_attrs(value, depth),
81+
}
82+
83+
84+
special_methods = [
85+
'__init__',
86+
'__str__',
87+
'__int__',
88+
'__float__',
89+
'__bool__',
90+
'__contains__',
91+
'__iter__',
92+
]
93+
94+
95+
def dump_attrs(d, depth):
96+
result = []
97+
seen = set()
98+
try:
99+
mro = d.mro()
100+
except TypeError:
101+
mro = [d]
102+
for base in mro:
103+
v = vars(base)
104+
for name, value in v.items():
105+
if name not in seen:
106+
result.append([name, dump_value(value, depth + 1)])
107+
seen.add(name)
108+
for m in special_methods:
109+
if hasattr(d, m) and m not in seen:
110+
result.append([m, dump_value(getattr(d, m), depth + 1)])
111+
return result
112+
113+
114+
kind_map = {
115+
inspect.Parameter.POSITIONAL_ONLY: 'POS_ONLY',
116+
inspect.Parameter.POSITIONAL_OR_KEYWORD: 'POS_OR_KW',
117+
inspect.Parameter.VAR_POSITIONAL: 'VAR_POS',
118+
inspect.Parameter.KEYWORD_ONLY: 'KW_ONLY',
119+
inspect.Parameter.VAR_KEYWORD: 'VAR_KW',
120+
}
121+
122+
123+
def param_kind(p):
124+
s = kind_map[p.kind]
125+
if p.default != inspect.Parameter.empty:
126+
assert s in ('POS_ONLY', 'POS_OR_KW', 'KW_ONLY')
127+
s += '_OPT'
128+
return s
129+
130+
131+
def dump_function(value):
132+
try:
133+
sig = inspect.signature(value)
134+
except ValueError:
135+
# The signature call sometimes fails for some reason.
136+
return 'invalid_signature'
137+
params = list(sig.parameters.items())
138+
return {
139+
'type': 'function',
140+
'args': [(name,
141+
param_kind(p),
142+
dump_simple(p.default))
143+
for name, p in params],
144+
}
145+
146+
147+
if __name__ == '__main__':
148+
import sys
149+
if len(sys.argv) != 2:
150+
sys.exit('usage: dumpmodule.py module-name')
151+
dump_module(sys.argv[1])

scripts/stubtest.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""Tests for stubs.
2+
3+
Verify that various things in stubs are consistent with how things behave
4+
at runtime.
5+
"""
6+
7+
import importlib
8+
import json
9+
import subprocess
10+
import sys
11+
from typing import Dict, Any
12+
13+
from mypy import build
14+
from mypy.build import default_data_dir, default_lib_path, find_modules_recursive
15+
from mypy.errors import CompileError
16+
from mypy.nodes import MypyFile, TypeInfo, FuncItem
17+
from mypy.options import Options
18+
19+
20+
skipped = {
21+
'_importlib_modulespec',
22+
'_subprocess',
23+
'distutils.command.bdist_msi',
24+
'distutils.command.bdist_packager',
25+
'msvcrt',
26+
'wsgiref.types',
27+
}
28+
29+
30+
class Errors:
31+
def __init__(self, id):
32+
self.id = id
33+
self.num_errors = 0
34+
35+
def fail(self, msg):
36+
print('{}: {}'.format(self.id, msg))
37+
self.num_errors += 1
38+
39+
40+
def test_stub(id: str) -> None:
41+
result = build_stubs(id)
42+
verify_stubs(result.files, prefix=id)
43+
44+
45+
def verify_stubs(files: Dict[str, MypyFile], prefix: str) -> None:
46+
for id, node in files.items():
47+
if not (id == prefix or id.startswith(prefix + '.')):
48+
# Not one of the target modules
49+
continue
50+
if id in skipped:
51+
# There's some issue with processing this module; skip for now
52+
continue
53+
dumped = dump_module(id)
54+
verify_stub(id, node.names, dumped)
55+
56+
57+
def verify_stub(id, symbols, dumped):
58+
errors = Errors(id)
59+
for name, node in symbols.items():
60+
if name.startswith('_'):
61+
# Private attribute
62+
continue
63+
if not node.module_public:
64+
# Again, a private attribute
65+
continue
66+
if name not in dumped:
67+
errors.fail('"{}" defined in stub but not at runtime'.format(name))
68+
else:
69+
verify_node(name, node, dumped[name], errors)
70+
71+
72+
def verify_node(name, node, dump, errors):
73+
if isinstance(node.node, TypeInfo):
74+
if not isinstance(dump, dict) or dump['type'] != 'class':
75+
errors.fail('"{}" is a class in stub but not at runtime'.format(name))
76+
return
77+
all_attrs = {x[0] for x in dump['attributes']}
78+
for attr, attr_node in node.node.names.items():
79+
if isinstance(attr_node.node, FuncItem) and attr not in all_attrs:
80+
errors.fail(
81+
('"{}.{}" defined as a method in stub but not defined '
82+
'at runtime in class object').format(
83+
name, attr))
84+
# TODO other kinds of nodes
85+
86+
87+
def dump_module(id: str) -> Dict[str, Any]:
88+
try:
89+
o = subprocess.check_output(
90+
['python', 'scripts/dumpmodule.py', id])
91+
except subprocess.CalledProcessError:
92+
print('Failure to dump module contents of "{}"'.format(id))
93+
sys.exit(1)
94+
return json.loads(o.decode('ascii'))
95+
96+
97+
def build_stubs(id):
98+
data_dir = default_data_dir(None)
99+
options = Options()
100+
options.python_version = (3, 6)
101+
lib_path = default_lib_path(data_dir,
102+
options.python_version,
103+
custom_typeshed_dir=None)
104+
sources = find_modules_recursive(id, lib_path)
105+
if not sources:
106+
sys.exit('Error: Cannot find module {}'.format(repr(id)))
107+
msg = []
108+
try:
109+
res = build.build(sources=sources,
110+
options=options)
111+
msg = res.errors
112+
except CompileError as e:
113+
msg = e.messages
114+
if msg:
115+
for m in msg:
116+
print(m)
117+
sys.exit(1)
118+
return res
119+
120+
121+
if __name__ == '__main__':
122+
test_stub(sys.argv[1])

0 commit comments

Comments
 (0)