Skip to content

Commit

Permalink
Hinting function params, function return value, class attribute using…
Browse files Browse the repository at this point in the history
  • Loading branch information
emacsway committed Feb 16, 2016
1 parent bd89775 commit d2496d2
Show file tree
Hide file tree
Showing 7 changed files with 558 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ See Mercurial logs and the mailing list for details.
* Orestis Markou <[email protected]>
* Ronny Pfannschmidt <[email protected]>
* Anton Gritsay <[email protected]>
* Ivan Zakrevsky <[email protected]>
* Sergey Glazyrin <[email protected]>
92 changes: 92 additions & 0 deletions docs/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,98 @@ Rope uses this feature by default but you can disable it by editing
``config.py``.


Type Hinting
------------

Currently supported type hinting for:

- function parameter type, using function doctring (:type or @type)
- function return type, using function doctring (:rtype or @rtype)
- class attribute type, using class docstring (:type or @type). Attribute should by set to None or NotImplemented in class or constructor of class.
- any assignment, using type comments of PEP 0484 (in limited form).

If rope cannot detect the type of a function argument correctly (due to the
dynamic nature of Python), you can help it by hinting the type using
one of the following docstring syntax styles.


**Sphinx style**

http://sphinx-doc.org/domains.html#info-field-lists

::

def myfunction(node, foo):
"""Do something with a ``node``.

:type node: ProgramNode
:param str foo: foo parameter description

"""
node.| # complete here


**Epydoc**

http://epydoc.sourceforge.net/manual-fields.html

::

def myfunction(node):
"""Do something with a ``node``.

@type node: ProgramNode

"""
node.| # complete here


**Numpydoc**

https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt

In order to support the numpydoc format, you need to install the `numpydoc
<https://pypi.python.org/pypi/numpydoc>`__ package.

::

def foo(var1, var2, long_var_name='hi'):
r"""A one-line summary that does not use variable names or the
function name.

...

Parameters
----------
var1 : array_like
Array_like means all those objects -- lists, nested lists,
etc. -- that can be converted to an array. We can also
refer to variables like `var1`.
var2 : int
The type above can either refer to an actual Python type
(e.g. ``int``), or describe the type of the variable in more
detail, e.g. ``(N,) ndarray`` or ``array_like``.
long_variable_name : {'hi', 'ho'}, optional
Choices in brackets, default first when optional.

...

"""
var2.| # complete here


**PEP 0484**

https://www.python.org/dev/peps/pep-0484/#type-comments

::

class Sample(object):
def __init__(self):
self.x = None # type: random.Random
self.x.| # complete here


Custom Source Folders
=====================

Expand Down
1 change: 1 addition & 0 deletions docs/rope.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Features implemented so far:
* Static and dynamic object analysis
* Handling built-in container types
* Saving object information on disk and validating them
* Type hints using docstring or type comments PEP 0484

For more information see `overview.rst`_.

Expand Down
223 changes: 223 additions & 0 deletions rope/base/oi/docstrings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
"""
Hinting the type using docstring of class/function.
It's an irreplaceable thing if you are using Dependency Injection with passive class:
http://www.martinfowler.com/articles/injection.html
Some code extracted (or based on code) from:
https://github.com/davidhalter/jedi/blob/b489019f5bd5750051122b94cc767df47751ecb7/jedi/evaluate/docstrings.py
Thanks to @davidhalter for this utils under MIT License.
Similar solutions:
- https://www.jetbrains.com/pycharm/help/type-hinting-in-pycharm.html
- https://www.python.org/dev/peps/pep-0484/#type-comments
- http://www.pydev.org/manual_adv_type_hints.html
- https://jedi.readthedocs.org/en/latest/docs/features.html#type-hinting
Discussions:
- https://groups.google.com/d/topic/rope-dev/JlAzmZ83K1M/discussion
- https://groups.google.com/d/topic/rope-dev/LCFNN98vckI/discussion
"""
import re
from ast import literal_eval

from rope.base.exceptions import AttributeNotFoundError
from rope.base.evaluate import ScopeNameFinder
from rope.base.pyobjects import PyClass

PEP0484_PATTERNS = [
re.compile(r'type:\s*([^\n, ]+)'),
]

DOCSTRING_PARAM_PATTERNS = [
r'\s*:type\s+%s:\s*([^\n, ]+)', # Sphinx
r'\s*:param\s+(\w+)\s+%s:[^\n]+', # Sphinx param with type
r'\s*@type\s+%s:\s*([^\n, ]+)', # Epydoc
]

DOCSTRING_RETURN_PATTERNS = [
re.compile(r'\s*:rtype:\s*([^\n, ]+)', re.M), # Sphinx
re.compile(r'\s*@rtype:\s*([^\n, ]+)', re.M), # Epydoc
]

REST_ROLE_PATTERN = re.compile(r':[^`]+:`([^`]+)`')

try:
from numpydoc.docscrape import NumpyDocString
except ImportError:
def _search_param_in_numpydocstr(docstr, param_str):
return []
else:
def _search_param_in_numpydocstr(docstr, param_str):
"""Search `docstr` (in numpydoc format) for type(-s) of `param_str`."""
params = NumpyDocString(docstr)._parsed_data['Parameters']
for p_name, p_type, p_descr in params:
if p_name == param_str:
m = re.match('([^,]+(,[^,]+)*?)(,[ ]*optional)?$', p_type)
if m:
p_type = m.group(1)

if p_type.startswith('{'):
types = set(type(x).__name__ for x in literal_eval(p_type))
return list(types)
else:
return [p_type]
return []


def hint_pep0484(pyname):
from rope.base.oi.soi import _get_lineno_for_node
lineno = _get_lineno_for_node(pyname.assignments[0].ast_node)
holding_scope = pyname.module.get_scope().get_inner_scope_for_line(lineno)
line = holding_scope._get_global_scope()._scope_finder.lines.get_line(lineno)
if '#' in line:
type_strs = _search_type_in_pep0484(line.split('#', 1)[1])
if type_strs:
return _resolve_type(type_strs[0], holding_scope.pyobject)


def _search_type_in_pep0484(code):
""" For more info see:
https://www.python.org/dev/peps/pep-0484/#type-comments
>>> _search_type_in_pep0484('type: int')
['int']
"""
for p in PEP0484_PATTERNS:
match = p.search(code)
if match:
return [match.group(1)]


def hint_param(pyfunc, param_name):
type_strs = None
func = pyfunc
while not type_strs and func:
if func.get_doc():
type_strs = _search_param_in_docstr(func.get_doc(), param_name)
func = _get_superfunc(func)

if type_strs:
return _resolve_type(type_strs[0], pyfunc)


def _get_superfunc(pyfunc):

if not isinstance(pyfunc.parent, PyClass):
return

for cls in _get_mro(pyfunc.parent)[1:]:
try:
return cls.get_attribute(pyfunc.get_name()).get_object()
except AttributeNotFoundError:
pass


def _get_mro(pyclass):
# FIXME: to use real mro() result
l = [pyclass]
for cls in l:
for super_cls in cls.get_superclasses():
if isinstance(super_cls, PyClass) and super_cls not in l:
l.append(super_cls)
return l


def _resolve_type(type_name, pyobj):
type_ = None
if '.' not in type_name:
try:
type_ = pyobj.get_module().get_scope().get_name(type_name).get_object()
except Exception:
pass
else:
mod_name, attr_name = type_name.rsplit('.', 1)
try:
mod_finder = ScopeNameFinder(pyobj.get_module())
mod = mod_finder._find_module(mod_name).get_object()
type_ = mod.get_attribute(attr_name).get_object()
except Exception:
pass
return type_


def _search_param_in_docstr(docstr, param_str):
"""
Search `docstr` for type(-s) of `param_str`.
>>> _search_param_in_docstr(':type param: int', 'param')
['int']
>>> _search_param_in_docstr('@type param: int', 'param')
['int']
>>> _search_param_in_docstr(
... ':type param: :class:`threading.Thread`', 'param')
['threading.Thread']
>>> bool(_search_param_in_docstr('no document', 'param'))
False
>>> _search_param_in_docstr(':param int param: some description', 'param')
['int']
"""
patterns = [re.compile(p % re.escape(param_str))
for p in DOCSTRING_PARAM_PATTERNS]
for pattern in patterns:
match = pattern.search(docstr)
if match:
return [_strip_rst_role(match.group(1))]

return (_search_param_in_numpydocstr(docstr, param_str) or
[])


def _strip_rst_role(type_str):
"""
Strip off the part looks like a ReST role in `type_str`.
>>> _strip_rst_role(':class:`ClassName`') # strip off :class:
'ClassName'
>>> _strip_rst_role(':py:obj:`module.Object`') # works with domain
'module.Object'
>>> _strip_rst_role('ClassName') # do nothing when not ReST role
'ClassName'
See also:
http://sphinx-doc.org/domains.html#cross-referencing-python-objects
"""
match = REST_ROLE_PATTERN.match(type_str)
if match:
return match.group(1)
else:
return type_str


def hint_return(pyfunc):
type_str = None
func = pyfunc
while not type_str and func:
if func.get_doc():
type_str = _search_return_in_docstr(func.get_doc())
func = _get_superfunc(func)
if type_str:
return _resolve_type(type_str, pyfunc)


def _search_return_in_docstr(code):
for p in DOCSTRING_RETURN_PATTERNS:
match = p.search(code)
if match:
return _strip_rst_role(match.group(1))


def hint_attr(pyclass, attr_name):
type_strs = None
for cls in _get_mro(pyclass):
if cls.get_doc():
type_strs = _search_param_in_docstr(cls.get_doc(), attr_name)
if type_strs:
break
if type_strs:
return _resolve_type(type_strs[0], pyclass)
Loading

0 comments on commit d2496d2

Please sign in to comment.