-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Pluggable system for generating types from docstrings #2240
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
Changes from 1 commit
0a47ddb
89e2a12
d44291c
05c66cd
6f56a15
6b0c77b
775f575
bbd5964
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
from functools import wraps | ||
from inspect import cleandoc | ||
import sys | ||
import inspect | ||
|
||
from typing import Tuple, Union, TypeVar, Callable, Sequence, Optional, Any, cast, List | ||
from mypy.nodes import ( | ||
|
@@ -23,7 +23,8 @@ | |
) | ||
from mypy import defaults | ||
from mypy import experiments | ||
from mypy import docstrings | ||
from mypy import hooks | ||
from mypy.parsetype import parse_str_as_type | ||
from mypy.errors import Errors | ||
|
||
try: | ||
|
@@ -89,6 +90,35 @@ def parse_type_comment(type_comment: str, line: int) -> Type: | |
return TypeConverter(line=line).visit(typ.body) | ||
|
||
|
||
def parse_docstring(docstring: str, arg_names: List[str], | ||
line: int) -> Optional[Tuple[List[Type], Type]]: | ||
"""Parse a docstring and return type representations. | ||
|
||
Returns a 2-tuple: (list of arguments Types, and return Type). | ||
""" | ||
def pop_and_convert(name): | ||
t = type_map.pop(name, None) | ||
if t is None: | ||
return AnyType() | ||
elif isinstance(t, Type): | ||
return t | ||
else: | ||
return parse_str_as_type(t, line) | ||
|
||
docstring_parser = hooks.get_docstring_parser() | ||
if docstring_parser is not None: | ||
type_map = docstring_parser(inspect.cleandoc(docstring), line) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still believe calling cleandoc() is up to the custom parser. They may have some other approach to parsing the contents of the docstring. |
||
if type_map: | ||
arg_types = [pop_and_convert(name) for name in arg_names] | ||
return_type = pop_and_convert('return') | ||
if type_map: | ||
raise TypeCommentParseError( | ||
'Arguments parsed from docstring are not present in ' | ||
'function signature: {}'.format(', '.join(type_map)), | ||
line, 0) | ||
return arg_types, return_type | ||
|
||
|
||
def with_line(f: Callable[['ASTConverter', T], U]) -> Callable[['ASTConverter', T], U]: | ||
@wraps(f) | ||
def wrapper(self: 'ASTConverter', ast: T) -> U: | ||
|
@@ -289,15 +319,16 @@ def do_func_def(self, n: Union[ast35.FunctionDef, ast35.AsyncFunctionDef], | |
else: | ||
arg_types = [a.type_annotation for a in args] | ||
return_type = TypeConverter(line=n.lineno).visit(n.returns) | ||
# docstrings | ||
if not any(arg_types) and return_type is None: | ||
# hooks | ||
if (not any(arg_types) and return_type is None and | ||
hooks.get_docstring_parser()): | ||
doc = ast35.get_docstring(n, clean=False) | ||
if doc: | ||
doc = cleandoc(doc.decode('unicode_escape')) | ||
type_map, rtype = docstrings.parse_docstring(doc, n.lineno) | ||
if type_map is not None: | ||
arg_types = [type_map.get(name) for name in arg_names] | ||
return_type = rtype | ||
doc = doc.decode('unicode_escape') | ||
types = parse_docstring(doc, arg_names, n.lineno) | ||
if types is not None: | ||
arg_types, return_type = types | ||
|
||
for arg, arg_type in zip(args, arg_types): | ||
self.set_type_optional(arg_type, arg.initializer) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
from typing import Dict, Optional, Callable, Union | ||
from mypy.types import Type | ||
|
||
hooks = {} # type: Dict[str, Callable] | ||
|
||
docstring_parser_type = Callable[[str, int], Optional[Dict[str, Union[str, Type]]]] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A type alias like this should have a CapWords name. I would also prefer this to always return a string. |
||
|
||
|
||
def set_docstring_parser(func: docstring_parser_type) -> None: | ||
"""Enable the docstring parsing hook. | ||
|
||
The callable must take a docstring for a function along with its line number | ||
(typically passed to mypy.parsetype.parse_str_as_type), and should return | ||
a mapping of argument name to type. The function's return type, if | ||
specified, is stored in the mapping with the special key 'return'. | ||
|
||
The keys of the mapping must be a subset of the arguments of the function | ||
to which the docstring belongs (other than the special 'return' | ||
key); an error will be raised if the mapping contains stray arguments. | ||
|
||
The values of the mapping must be either mypy.types.Type or a valid | ||
PEP484-compatible string which can be converted to a Type. | ||
""" | ||
hooks['docstring_parser'] = func | ||
|
||
|
||
def get_docstring_parser() -> Optional[docstring_parser_type]: | ||
return hooks.get('docstring_parser') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The get and set functions look an afwul lot like Java-style accessor methods. If you want type-safety it's probably better to define a Hooks class whose instance variables are the known hooks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To clarify, are you looking for a single registry for all hooks, like this: class Hooks:
# The docstring_parser hook must take a docstring for a function [...etc...]
docstring_parser = None # type: Callable[[str], Optional[Dict[str, str]]]
# another explanation...
future_hook = None # type: Callable[whatever]
registry = Hooks() Where the end user would then override the attribute: import mypy.hooks
mypy.hooks.registry.docstring_parser = my_parser If not, can you clarify a bit more. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This calls into the old parser, which we want to avoid doing from the fast parser, as it'll eventually replace the old one. You can use the fast parser to do this instead: see
TypeConverter
below.