Skip to content

scripts/stubtest: make it run again and type-check it #7283

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 22 additions & 28 deletions scripts/dumpmodule.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,28 @@
"""Dump the runtime structure of a module as JSON.

This is used for testing stubs.

This needs to run in Python 2.7 and 3.x.
"""

from __future__ import print_function

import importlib
import inspect
import json
import sys
import types
from typing import Text
from types import FunctionType
from typing import Optional, Dict, Any, Set, Callable
from typing_extensions import Final


if sys.version_info >= (3, 0):
import inspect
long = int
else:
import inspect2 as inspect
DumpNode = Dict[str, Any]



def dump_module(id):
def dump_module(id: str) -> None:
m = importlib.import_module(id)
data = module_to_json(m)
print(json.dumps(data, ensure_ascii=True, indent=4, sort_keys=True))


def module_to_json(m):
result = {}
def module_to_json(m: object) -> Dict[str, DumpNode]:
result = {} # type: Dict[str, DumpNode]
for name, value in m.__dict__.items():
# Filter out some useless attributes.

Expand All @@ -46,7 +39,7 @@ def module_to_json(m):
result[name] = dump_value(value)

try:
_, line = inspect.getsourcelines(getattr(m, name))
line = inspect.getsourcelines(getattr(m, name))[1] # type: Optional[int]
except (TypeError, OSError):
line = None

Expand All @@ -55,12 +48,13 @@ def module_to_json(m):
return result


def dump_value(value, depth=0):
def dump_value(value: object, depth: int = 0) -> DumpNode:
if depth > 10:
return 'max_recursion_depth_exceeded'
# TODO: Callers don't handle this case.
return 'max_recursion_depth_exceeded' # type: ignore
if isinstance(value, type):
return dump_class(value, depth + 1)
if inspect.isfunction(value):
if isinstance(value, FunctionType):
return dump_function(value)
if callable(value):
return {'type': 'callable'} # TODO more information
Expand All @@ -74,8 +68,8 @@ def dump_value(value, depth=0):
return dump_simple(value)


def dump_simple(value):
if type(value) in (int, bool, float, str, bytes, Text, long, list, set, dict, tuple):
def dump_simple(value: object) -> DumpNode:
if type(value) in (int, bool, float, str, bytes, list, set, dict, tuple):
return {'type': type(value).__name__}
if value is None:
return {'type': 'None'}
Expand All @@ -84,7 +78,7 @@ def dump_simple(value):
return {'type': 'unknown'}


def dump_class(value, depth):
def dump_class(value: type, depth: int) -> DumpNode:
return {
'type': 'class',
'attributes': dump_attrs(value, depth),
Expand All @@ -99,13 +93,13 @@ def dump_class(value, depth):
'__bool__',
'__contains__',
'__iter__',
]
] # type: Final


# Change to return a dict
def dump_attrs(d, depth):
def dump_attrs(d: type, depth: int) -> DumpNode:
result = {}
seen = set()
seen = set() # type: Set[str]
try:
mro = d.mro()
except TypeError:
Expand All @@ -128,18 +122,18 @@ def dump_attrs(d, depth):
inspect.Parameter.VAR_POSITIONAL: 'VAR_POS',
inspect.Parameter.KEYWORD_ONLY: 'KW_ONLY',
inspect.Parameter.VAR_KEYWORD: 'VAR_KW',
}
} # type: Final


def param_kind(p):
def param_kind(p: inspect.Parameter) -> str:
s = kind_map[p.kind]
if p.default != inspect.Parameter.empty:
assert s in ('POS_ONLY', 'POS_OR_KW', 'KW_ONLY')
s += '_OPT'
return s


def dump_function(value):
def dump_function(value: FunctionType) -> DumpNode:
try:
sig = inspect.signature(value)
except ValueError:
Expand Down
122 changes: 74 additions & 48 deletions scripts/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@

import importlib
import sys
from typing import Dict, Any
from collections import defaultdict, namedtuple
from typing import Dict, Any, List, Iterator, NamedTuple, Optional, Mapping, Tuple
from typing_extensions import Type, Final
from collections import defaultdict
from functools import singledispatch

from mypy import build
from mypy.build import default_data_dir, default_lib_path, find_modules_recursive
from mypy.build import default_data_dir
from mypy.modulefinder import compute_search_paths, FindModuleCache
from mypy.errors import CompileError
from mypy import nodes
from mypy.options import Options

import dumpmodule
from dumpmodule import module_to_json, DumpNode

from functools import singledispatch

# TODO: email.contentmanager has a symbol table with a None node.
# This seems like it should not be.
Expand All @@ -33,7 +35,8 @@
'unittest.mock', # mock.call infinite loops on inspect.getsourcelines
# https://bugs.python.org/issue25532
# TODO: can we filter only call?
}
} # type: Final


messages = {
'not_in_runtime': ('{error.stub_type} "{error.name}" defined at line '
Expand All @@ -43,44 +46,57 @@
'no_stubs': 'could not find typeshed {error.name}',
'inconsistent': ('"{error.name}" is {error.stub_type} in stub but'
' {error.module_type} at runtime'),
}

Error = namedtuple('Error', (
'module',
'name',
'error_type',
'line',
'stub_type',
'module_type'))


def test_stub(name: str):
} # type: Final

Error = NamedTuple('Error', (
('module', str),
('name', str),
('error_type', str),
('line', Optional[int]),
('stub_type', Optional[Type[nodes.Node]]),
('module_type', Optional[str]),
))

ErrorParts = Tuple[
List[str],
str,
Optional[int],
Optional[Type[nodes.Node]],
Optional[str],
]


def test_stub(options: Options,
find_module_cache: FindModuleCache,
name: str) -> Iterator[Error]:
stubs = {
mod: stub for mod, stub in build_stubs(name).items()
mod: stub for mod, stub in build_stubs(options, find_module_cache, name).items()
if (mod == name or mod.startswith(name + '.')) and mod not in skip
}

for mod, stub in stubs.items():
instance = dump_module(mod)

for identifiers, *error in verify(stub, instance):
yield Error(mod, '.'.join(identifiers), *error)
for identifiers, error_type, line, stub_type, module_type in verify(stub, instance):
yield Error(mod, '.'.join(identifiers), error_type, line, stub_type, module_type)


@singledispatch
def verify(node, module_node):
def verify(node: nodes.Node,
module_node: Optional[DumpNode]) -> Iterator[ErrorParts]:
raise TypeError('unknown mypy node ' + str(node))



@verify.register(nodes.MypyFile)
def verify_mypyfile(stub, instance):
def verify_mypyfile(stub: nodes.MypyFile,
instance: Optional[DumpNode]) -> Iterator[ErrorParts]:
if instance is None:
yield [], 'not_in_runtime', stub.line, type(stub), None
elif instance['type'] != 'file':
yield [], 'inconsistent', stub.line, type(stub), instance['type']
else:
stub_children = defaultdict(lambda: None, stub.names)
stub_children = defaultdict(lambda: None, stub.names) # type: Mapping[str, Optional[nodes.SymbolTableNode]]
instance_children = defaultdict(lambda: None, instance['names'])

# TODO: I would rather not filter public children here.
Expand All @@ -90,29 +106,32 @@ def verify_mypyfile(stub, instance):
name: (stub_children[name], instance_children[name])
for name in set(stub_children) | set(instance_children)
if not name.startswith('_')
and (stub_children[name] is None or stub_children[name].module_public)
and (stub_children[name] is None or stub_children[name].module_public) # type: ignore
}

for node, (stub_child, instance_child) in public_nodes.items():
stub_child = getattr(stub_child, 'node', None)
for identifiers, *error in verify(stub_child, instance_child):
yield ([node] + identifiers, *error)
for identifiers, error_type, line, stub_type, module_type in verify(stub_child, instance_child):
yield ([node] + identifiers, error_type, line, stub_type, module_type)


@verify.register(nodes.TypeInfo)
def verify_typeinfo(stub, instance):
def verify_typeinfo(stub: nodes.TypeInfo,
instance: Optional[DumpNode]) -> Iterator[ErrorParts]:
if not instance:
yield [], 'not_in_runtime', stub.line, type(stub), None
elif instance['type'] != 'class':
yield [], 'inconsistent', stub.line, type(stub), instance['type']
else:
for attr, attr_node in stub.names.items():
subdump = instance['attributes'].get(attr, None)
for identifiers, *error in verify(attr_node.node, subdump):
yield ([attr] + identifiers, *error)
for identifiers, error_type, line, stub_type, module_type in verify(attr_node.node, subdump):
yield ([attr] + identifiers, error_type, line, stub_type, module_type)


@verify.register(nodes.FuncItem)
def verify_funcitem(stub, instance):
def verify_funcitem(stub: nodes.FuncItem,
instance: Optional[DumpNode]) -> Iterator[ErrorParts]:
if not instance:
yield [], 'not_in_runtime', stub.line, type(stub), None
elif 'type' not in instance or instance['type'] not in ('function', 'callable'):
Expand All @@ -121,15 +140,17 @@ def verify_funcitem(stub, instance):


@verify.register(type(None))
def verify_none(stub, instance):
def verify_none(stub: None,
instance: Optional[DumpNode]) -> Iterator[ErrorParts]:
if instance is None:
yield [], 'not_in_stub', None, None, None
else:
yield [], 'not_in_stub', instance['line'], None, instance['type']


@verify.register(nodes.Var)
def verify_var(node, module_node):
def verify_var(node: nodes.Var,
module_node: Optional[DumpNode]) -> Iterator[ErrorParts]:
if False:
yield None
# Need to check if types are inconsistent.
Expand All @@ -139,37 +160,36 @@ def verify_var(node, module_node):


@verify.register(nodes.OverloadedFuncDef)
def verify_overloadedfuncdef(node, module_node):
def verify_overloadedfuncdef(node: nodes.OverloadedFuncDef,
module_node: Optional[DumpNode]) -> Iterator[ErrorParts]:
# Should check types of the union of the overloaded types.
if False:
yield None


@verify.register(nodes.TypeVarExpr)
def verify_typevarexpr(node, module_node):
def verify_typevarexpr(node: nodes.TypeVarExpr,
module_node: Optional[DumpNode]) -> Iterator[ErrorParts]:
if False:
yield None


@verify.register(nodes.Decorator)
def verify_decorator(node, module_noode):
def verify_decorator(node: nodes.Decorator,
module_node: Optional[DumpNode]) -> Iterator[ErrorParts]:
if False:
yield None


def dump_module(name: str) -> Dict[str, Any]:
def dump_module(name: str) -> DumpNode:
mod = importlib.import_module(name)
return {'type': 'file', 'names': dumpmodule.module_to_json(mod)}
return {'type': 'file', 'names': module_to_json(mod)}


def build_stubs(mod):
data_dir = default_data_dir(None)
options = Options()
options.python_version = (3, 6)
lib_path = default_lib_path(data_dir,
options.python_version,
custom_typeshed_dir=None)
sources = find_modules_recursive(mod, lib_path)
def build_stubs(options: Options,
find_module_cache: FindModuleCache,
mod: str) -> Dict[str, nodes.MypyFile]:
sources = find_module_cache.find_modules_recursive(mod)
try:
res = build.build(sources=sources,
options=options)
Expand All @@ -184,15 +204,21 @@ def build_stubs(mod):
return res.files


def main(args):
def main(args: List[str]) -> Iterator[Error]:
if len(args) == 1:
print('must provide at least one module to test')
sys.exit(1)
else:
modules = args[1:]

options = Options()
options.python_version = (3, 6)
data_dir = default_data_dir()
search_path = compute_search_paths([], options, data_dir)
find_module_cache = FindModuleCache(search_path)

for module in modules:
for error in test_stub(module):
for error in test_stub(options, find_module_cache, module):
yield error


Expand Down
4 changes: 3 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ commands = flake8 {posargs}
[testenv:type]
description = type check ourselves
basepython = python3.7
commands = python -m mypy --config-file mypy_self_check.ini -p mypy
commands =
python -m mypy --config-file mypy_self_check.ini -p mypy
python -m mypy --config-file mypy_self_check.ini scripts/stubtest.py

[testenv:docs]
description = invoke sphinx-build to build the HTML docs
Expand Down