Skip to content

Commit 589cf8f

Browse files
committed
Restructure hooks as a sub-structure of mypy.options.Options
1 parent 9803da3 commit 589cf8f

File tree

5 files changed

+135
-36
lines changed

5 files changed

+135
-36
lines changed

mypy/fastparse.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
from mypy import defaults
3030
from mypy import experiments
3131
from mypy import messages
32-
from mypy import hooks
3332
from mypy.errors import Errors
3433
from mypy.options import Options
3534

@@ -115,13 +114,13 @@ def parse_type_comment(type_comment: str, line: int, errors: Optional[Errors]) -
115114
return TypeConverter(errors, line=line).visit(typ.body)
116115

117116

118-
def parse_docstring(docstring: str, arg_names: List[str],
117+
def parse_docstring(options: Options, docstring: str, arg_names: List[str],
119118
line: int, errors: Errors) -> Optional[Tuple[List[Type], Type]]:
120119
"""Parse a docstring and return type representations.
121120
122121
Returns a 2-tuple: (list of arguments Types, and return Type).
123122
"""
124-
opts = hooks.options.get('docstring_parser', {})
123+
hook = options.hooks.docstring_parser
125124

126125
def pop_and_convert(name: str) -> Optional[Type]:
127126
t = type_map.pop(name, None)
@@ -130,8 +129,8 @@ def pop_and_convert(name: str) -> Optional[Type]:
130129
else:
131130
return parse_type_comment(t[0], line + t[1], errors)
132131

133-
if hooks.docstring_parser is not None:
134-
type_map = hooks.docstring_parser(docstring, opts, errors)
132+
if hook.func is not None:
133+
type_map = options.hooks.docstring_parser.func(docstring, hook.options, errors)
135134
if type_map:
136135
arg_types = [pop_and_convert(name) for name in arg_names]
137136
return_type = pop_and_convert('return')
@@ -376,11 +375,10 @@ def do_func_def(self, n: Union[ast3.FunctionDef, ast3.AsyncFunctionDef],
376375
return_type = TypeConverter(self.errors, line=n.returns.lineno
377376
if n.returns else n.lineno).visit(n.returns)
378377
# hooks
379-
if (not any(arg_types) and return_type is None and
380-
hooks.docstring_parser):
378+
if not any(arg_types) and return_type is None:
381379
doc = ast3.get_docstring(n, clean=False)
382380
if doc:
383-
types = parse_docstring(doc, real_names, n.lineno, self.errors)
381+
types = parse_docstring(self.options, doc, real_names, n.lineno, self.errors)
384382
if types is not None:
385383
arg_types, return_type = types
386384

mypy/fastparse2.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,11 +324,10 @@ def visit_FunctionDef(self, n: ast27.FunctionDef) -> Statement:
324324
arg_types = [a.type_annotation for a in args]
325325
return_type = converter.visit(None)
326326
# hooks
327-
if (not any(arg_types) and return_type is None and
328-
hooks.docstring_parser):
327+
if not any(arg_types) and return_type is None:
329328
doc = ast27.get_docstring(n, clean=False)
330329
if doc:
331-
types = parse_docstring(doc.decode('unicode_escape'),
330+
types = parse_docstring(self.options, doc.decode('unicode_escape'),
332331
real_names, n.lineno, self.errors)
333332
if types is not None:
334333
arg_types, return_type = types

mypy/hooks.py

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,92 @@
1-
from typing import Dict, Optional, Callable, Tuple
2-
from mypy.errors import Errors
1+
import importlib.util
2+
import os.path
3+
import pprint
4+
from types import ModuleType
5+
6+
from typing import Callable, Dict, Generic, Optional, Tuple, TypeVar, TYPE_CHECKING
7+
8+
if TYPE_CHECKING:
9+
from mypy.errors import Errors
10+
11+
12+
HookOptions = Dict[str, str]
13+
14+
T = TypeVar('T', bound=Callable)
15+
16+
17+
def _safeimport(path: str) -> Optional[ModuleType]:
18+
try:
19+
module = __import__(path)
20+
except ImportError as err:
21+
if err.name == path: # type: ignore # ImportError stubs need to be updated
22+
# No such module in the path.
23+
return None
24+
else:
25+
raise
26+
for part in path.split('.')[1:]:
27+
try:
28+
module = getattr(module, part)
29+
except AttributeError:
30+
return None
31+
return module
32+
33+
34+
def locate(path: str) -> Optional[object]:
35+
"""Locate an object by string identifier, importing as necessary.
36+
37+
The two identifiers supported are:
38+
39+
dotted path:
40+
package.module.object.child
41+
42+
file path followed by dotted object path]
43+
/path/to/module.py:object.child
44+
"""
45+
if ':' in path:
46+
file_path, obj_path = path.split(':', 1)
47+
mod_name = os.path.splitext(os.path.basename(file_path))[0]
48+
spec = importlib.util.spec_from_file_location(mod_name, file_path)
49+
module = importlib.util.module_from_spec(spec)
50+
spec.loader.exec_module(module)
51+
parts = obj_path.split('.')
52+
else:
53+
parts = [part for part in path.split('.') if part]
54+
module, n = None, 0
55+
while n < len(parts):
56+
nextmodule = _safeimport('.'.join(parts[:n + 1]))
57+
if nextmodule:
58+
module, n = nextmodule, n + 1
59+
else:
60+
break
61+
parts = parts[n:]
62+
63+
if not module:
64+
return None
65+
66+
obj = module
67+
for part in parts:
68+
try:
69+
obj = getattr(obj, part)
70+
except AttributeError:
71+
return None
72+
return obj
73+
74+
75+
class Hook(Generic[T]):
76+
77+
def __init__(self) -> None:
78+
self.func = None # type: T
79+
self.options = {} # type: HookOptions
80+
81+
def __eq__(self, other: object) -> bool:
82+
return self.__class__ == other.__class__ and self.__dict__ == other.__dict__
83+
84+
def __ne__(self, other: object) -> bool:
85+
return not self == other
86+
87+
def __repr__(self) -> str:
88+
return 'Hook({}, {})'.format(self.func, self.options)
389

4-
Options = Dict[str, str]
5-
options = {} # type: Dict[str, Options]
690

791
# The docstring_parser hook is called for each unannotated function that has a
892
# docstring. The callable should accept three arguments:
@@ -20,4 +104,18 @@
20104
# The function's return type, if specified, is stored in the mapping with the special
21105
# key 'return'. Other than 'return', each key of the mapping must be one of the
22106
# arguments of the documented function; otherwise, an error will be raised.
23-
docstring_parser = None # type: Callable[[str, Options, Errors], Dict[str, Tuple[str, int]]]
107+
DocstringParserHook = Hook[Callable[[str, HookOptions, 'Errors'], Dict[str, Tuple[str, int]]]]
108+
109+
110+
class Hooks:
111+
def __init__(self) -> None:
112+
self.docstring_parser = DocstringParserHook()
113+
114+
def __eq__(self, other: object) -> bool:
115+
return self.__class__ == other.__class__ and self.__dict__ == other.__dict__
116+
117+
def __ne__(self, other: object) -> bool:
118+
return not self == other
119+
120+
def __repr__(self) -> str:
121+
return 'Hooks({})'.format(pprint.pformat(self.__dict__))

mypy/main.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,16 @@
77
import re
88
import sys
99
import time
10-
from pydoc import locate
1110

12-
from typing import Any, Dict, List, Mapping, Optional, Set, Tuple, Callable
11+
from typing import Any, Callable, cast, Dict, List, Mapping, Optional, Set, Tuple
1312

1413
from mypy import build
1514
from mypy import defaults
1615
from mypy import experiments
17-
from mypy import hooks
1816
from mypy import util
1917
from mypy.build import BuildSource, BuildResult, PYTHON_EXTENSIONS
2018
from mypy.errors import CompileError
19+
from mypy.hooks import locate, Hook
2120
from mypy.options import Options, BuildType
2221
from mypy.report import reporter_classes
2322

@@ -578,16 +577,20 @@ def get_init_file(dir: str) -> Optional[str]:
578577

579578

580579
def load_hook(prefix: str, hook_name: str, hook_path: str) -> Optional[Callable]:
581-
# FIXME: no stubs for pydoc. should we write stubs or a simple replacement for locate?
582-
obj = locate(hook_path)
580+
try:
581+
obj = locate(hook_path)
582+
except BaseException as err:
583+
print("%s: Error finding hook %s at %s: %s" %
584+
(prefix, hook_name, hook_path, err), file=sys.stderr)
585+
return None
583586
if obj is None:
584587
print("%s: Could not find hook %s at %s" %
585588
(prefix, hook_name, hook_path), file=sys.stderr)
586-
if not callable(obj):
589+
elif not callable(obj):
587590
print("%s: Hook %s at %s is not callable" %
588591
(prefix, hook_name, hook_path), file=sys.stderr)
589592
return None
590-
return obj
593+
return cast(Callable, obj)
591594

592595

593596
# For most options, the type of the default value set in options.py is
@@ -644,32 +647,31 @@ def parse_config_file(options: Options, filename: Optional[str]) -> None:
644647
else:
645648
section = parser['mypy']
646649
prefix = '%s: [%s]' % (file_read, 'mypy')
647-
updates, report_dirs, hook_funcs = parse_section(prefix, options, section)
650+
updates, report_dirs, hooks = parse_section(prefix, options, section)
648651
for k, v in updates.items():
649652
setattr(options, k, v)
650653

651-
# bind hook functions to hooks module
652-
for k, v in hook_funcs.items():
654+
for k, v in hooks.items():
653655
hook_func = load_hook(prefix, k, v)
654656
if hook_func is not None:
655-
# FIXME: dynamically check loaded function annotations against those in `hooks`?
656-
setattr(hooks, k, hook_func)
657+
hook = getattr(options.hooks, k) # type: Hook
658+
hook.func = hook_func
657659
# look for an options section for this hook
658660
if k in parser:
659-
hooks.options[k] = dict(parser[k])
661+
hook.options = dict(parser[k])
660662
options.report_dirs.update(report_dirs)
661663

662664
for name, section in parser.items():
663665
if name.startswith('mypy-'):
664666
prefix = '%s: [%s]' % (file_read, name)
665-
updates, report_dirs, hook_funcs = parse_section(prefix, options, section)
667+
updates, report_dirs, hooks = parse_section(prefix, options, section)
666668
if report_dirs:
667669
print("%s: Per-module sections should not specify reports (%s)" %
668670
(prefix, ', '.join(s + '_report' for s in sorted(report_dirs))),
669671
file=sys.stderr)
670-
if hook_funcs:
672+
if hooks:
671673
print("%s: Per-module sections should not specify hooks (%s)" %
672-
(prefix, ', '.join(sorted(hook_funcs))),
674+
(prefix, ', '.join(sorted(hooks))),
673675
file=sys.stderr)
674676
if set(updates) - Options.PER_MODULE_OPTIONS:
675677
print("%s: Per-module sections should only specify per-module flags (%s)" %
@@ -695,17 +697,17 @@ def parse_section(prefix: str, template: Options,
695697
"""
696698
results = {} # type: Dict[str, object]
697699
report_dirs = {} # type: Dict[str, str]
698-
hook_funcs = {} # type: Dict[str, str]
700+
hooks = {} # type: Dict[str, str]
699701
for key in section:
700702
key = key.replace('-', '_')
701703
if key.startswith('hooks.'):
702704
dv = section.get(key)
703705
key = key[6:]
704-
if not hasattr(hooks, key):
706+
if not hasattr(template.hooks, key):
705707
print("%s: Unrecognized hook: %s = %s" % (prefix, key, dv),
706708
file=sys.stderr)
707709
else:
708-
hook_funcs[key] = dv
710+
hooks[key] = dv
709711
continue
710712
elif key in config_types:
711713
ct = config_types[key]
@@ -755,7 +757,7 @@ def parse_section(prefix: str, template: Options,
755757
if 'follow_imports' not in results:
756758
results['follow_imports'] = 'error'
757759
results[key] = v
758-
return results, report_dirs, hook_funcs
760+
return results, report_dirs, hooks
759761

760762

761763
def fail(msg: str) -> None:

mypy/options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Any, Mapping, Optional, Tuple, List, Pattern, Dict
66

77
from mypy import defaults
8+
from mypy.hooks import Hooks
89

910

1011
class BuildType:
@@ -132,6 +133,7 @@ def __init__(self) -> None:
132133
self.shadow_file = None # type: Optional[Tuple[str, str]]
133134
self.show_column_numbers = False # type: bool
134135
self.dump_graph = False
136+
self.hooks = Hooks()
135137

136138
def __eq__(self, other: object) -> bool:
137139
return self.__class__ == other.__class__ and self.__dict__ == other.__dict__

0 commit comments

Comments
 (0)