From 3b6bbab5562196d5bfbc5eb6e01c5f3d8d3067c6 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 3 Jan 2020 00:45:14 +0100 Subject: [PATCH] Infer doctests and signatures uniformly, fixes #1466 --- jedi/api/__init__.py | 2 +- jedi/api/classes.py | 5 ++- jedi/inference/names.py | 27 +++++++++---- .../with_python-stubs/__init__.pyi | 4 ++ .../with_python-stubs/module.pyi | 4 ++ .../stub_packages/with_python/__init__.py | 5 +++ .../stub_packages/with_python/module.py | 8 ++++ test/test_api/test_documentation.py | 4 +- test/test_inference/test_docstring.py | 12 +++--- .../test_gradual/test_stub_loading.py | 38 +++++++++++++++++++ 10 files changed, 89 insertions(+), 20 deletions(-) diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index c4ce99d1..ea840754 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -350,7 +350,7 @@ class Script(object): :rtype: list of :class:`classes.Definition` """ - definitions = self.goto(line, column) + definitions = self.goto(line, column, follow_imports=True) if definitions: return definitions leaf = self._module_node.get_leaf_for_position((line, column)) diff --git a/jedi/api/classes.py b/jedi/api/classes.py index 0b760d63..03abca8d 100644 --- a/jedi/api/classes.py +++ b/jedi/api/classes.py @@ -247,7 +247,7 @@ class BaseDefinition(object): signature_text = '\n'.join( signature.to_string() - for signature in self._name.get_signatures() + for signature in self._get_signatures() ) if signature_text and doc: return signature_text + '\n\n' + doc @@ -440,7 +440,8 @@ class BaseDefinition(object): return ''.join(lines[start_index:index + after + 1]) def _get_signatures(self): - return self._name.infer().get_signatures() + names = convert_names([self._name], prefer_stubs=True) + return [sig for name in names for sig in name.infer().get_signatures()] def get_signatures(self): return [ diff --git a/jedi/inference/names.py b/jedi/inference/names.py index 34ad98bf..a1d957fd 100644 --- a/jedi/inference/names.py +++ b/jedi/inference/names.py @@ -3,7 +3,7 @@ from abc import abstractmethod from parso.tree import search_ancestor from jedi._compatibility import Parameter -from jedi.parser_utils import find_statement_documentation +from jedi.parser_utils import find_statement_documentation, clean_scope_docstring from jedi.inference.utils import unite from jedi.inference.base_value import ValueSet, NO_VALUES from jedi.inference import docstrings @@ -311,18 +311,22 @@ class TreeNameDefinition(AbstractTreeName): return indexes def py__doc__(self): - if self.api_type in ('function', 'class', 'module'): + api_type = self.api_type + if api_type in ('function', 'class'): # Make sure the names are not TreeNameDefinitions anymore. - return _merge_name_docs([v.name for v in self.infer()]) + return clean_scope_docstring(self.tree_name.get_definition()) - if self.api_type == 'statement' and self.tree_name.is_definition(): + if api_type == 'module': + names = self.goto() + if self not in names: + return _merge_name_docs(names) + + if api_type == 'statement' and self.tree_name.is_definition(): return find_statement_documentation(self.tree_name.get_definition()) return '' def get_signatures(self): - if self.api_type in ('function', 'class'): - return self.infer().get_signatures() - return [] + return self.infer().get_signatures() class _ParamMixin(object): @@ -597,7 +601,14 @@ class NameWrapper(object): class StubNameMixin(object): def py__doc__(self): from jedi.inference.gradual.conversion import convert_names - names = convert_names([self], prefer_stub_to_compiled=False) + # Stubs are not complicated and we can just follow simple statements + # that have an equals in them, because they typically make something + # else public. See e.g. stubs for `requests`. + names = [self] + if self.api_type == 'statement' and '=' in self.tree_name.get_definition().children: + names = [v.name for v in self.infer()] + + names = convert_names(names, prefer_stub_to_compiled=False) if self in names: return super(StubNameMixin, self).py__doc__() else: diff --git a/test/examples/stub_packages/with_python-stubs/__init__.pyi b/test/examples/stub_packages/with_python-stubs/__init__.pyi index 5e7ee1b5..d3e7ff03 100644 --- a/test/examples/stub_packages/with_python-stubs/__init__.pyi +++ b/test/examples/stub_packages/with_python-stubs/__init__.pyi @@ -1,2 +1,6 @@ +from . import module + +func_with_stub = module.func_with_stub + both: int stub_only: str diff --git a/test/examples/stub_packages/with_python-stubs/module.pyi b/test/examples/stub_packages/with_python-stubs/module.pyi index 53331495..1a0c0453 100644 --- a/test/examples/stub_packages/with_python-stubs/module.pyi +++ b/test/examples/stub_packages/with_python-stubs/module.pyi @@ -1 +1,5 @@ in_sub_module: int + + +def func_with_stub(b: int) -> float: + pass diff --git a/test/examples/stub_packages/with_python/__init__.py b/test/examples/stub_packages/with_python/__init__.py index d16c270e..a17d2e62 100644 --- a/test/examples/stub_packages/with_python/__init__.py +++ b/test/examples/stub_packages/with_python/__init__.py @@ -1,2 +1,7 @@ +from with_python import module as _module + +func_without_stub = _module.func_without_stub + + python_only = 1 both = '' diff --git a/test/examples/stub_packages/with_python/module.py b/test/examples/stub_packages/with_python/module.py index 375009f8..8058f05c 100644 --- a/test/examples/stub_packages/with_python/module.py +++ b/test/examples/stub_packages/with_python/module.py @@ -1 +1,9 @@ +def func_without_stub(a): + 'nostubdoc' + + +def func_with_stub(c): + 'withstubdoc' + + in_sub_module = '' diff --git a/test/test_api/test_documentation.py b/test/test_api/test_documentation.py index 64b696a9..0f5e933c 100644 --- a/test/test_api/test_documentation.py +++ b/test/test_api/test_documentation.py @@ -59,7 +59,7 @@ def test_help_no_returns(Script, code, kwargs): ('X.x', 'Yeah '), ('X().x', 'Yeah '), ('X.y', 'f g '), - ('X.z', ''), + ('X.z', '(x)'), ] ) def test_attribute_docstrings(goto_or_help, expected_doc, to_execute): @@ -70,7 +70,7 @@ def test_attribute_docstrings(goto_or_help, expected_doc, to_execute): """ Yeah """ y = 5 "f g " - z = lambda: 1 + z = lambda x: 1 ''') d, = goto_or_help(code + to_execute) diff --git a/test/test_inference/test_docstring.py b/test/test_inference/test_docstring.py index c16473c9..0d85104d 100644 --- a/test/test_inference/test_docstring.py +++ b/test/test_inference/test_docstring.py @@ -76,7 +76,7 @@ def test_multiple_docstrings(Script): x = func '''Docstring of `x`.''' x""").help() - assert d.docstring() == 'Docstring of `x`.' + assert d.docstring() == 'func()\n\nDocstring of `x`.' def test_completion(Script): @@ -168,15 +168,13 @@ def test_import_function_docstring(Script, skip_pre_python35): path = os.path.join(test_dir, 'completion', 'import_function_docstring.py') c, = Script(code, path=path).complete() - stub_signature = 'stub_function(x: int, y: float) -> str' - python_signature = 'stub_function(x: float, y)' - doc = '\n\nPython docstring' - assert c.docstring() == stub_signature + doc + doc = 'stub_function(x: int, y: float) -> str\n\nPython docstring' + assert c.docstring() == doc assert c.type == 'function' func, = c.goto(prefer_stubs=True) - assert func.docstring() == stub_signature + doc + assert func.docstring() == doc func, = c.goto() - assert func.docstring() == python_signature + doc + assert func.docstring() == doc # ---- Numpy Style Tests --- diff --git a/test/test_inference/test_gradual/test_stub_loading.py b/test/test_inference/test_gradual/test_stub_loading.py index cb6b7aac..12f2499a 100644 --- a/test/test_inference/test_gradual/test_stub_loading.py +++ b/test/test_inference/test_gradual/test_stub_loading.py @@ -25,3 +25,41 @@ def ScriptInStubFolder(Script): def test_find_stubs_infer(ScriptInStubFolder, code, expected): defs = ScriptInStubFolder(code).infer() assert [d.name for d in defs] == expected + + +func_without_stub_sig = 'func_without_stub(a)' +func_without_stub_doc = func_without_stub_sig + '\n\nnostubdoc' +func_with_stub_doc = 'func_with_stub(b: int) -> float\n\nwithstubdoc' + + +@pytest.mark.parametrize( + ('code', 'expected'), [ + ('from with_python import stub_only', ''), + ('from with_python import python_only', ''), + ('from with_python import both', ''), + + ('import with_python; with_python.func_without_stub', func_without_stub_sig), + ('import with_python.module; with_python.module.func_without_stub', func_without_stub_doc), + ('from with_python import module; module.func_without_stub', func_without_stub_doc), + ('from with_python.module import func_without_stub', func_without_stub_doc), + ('from with_python.module import func_without_stub as f; f', func_without_stub_doc), + ('from with_python.module import func_without_stub; func_without_stub', + func_without_stub_doc), + ('from with_python import func_without_stub', func_without_stub_sig), + ('from with_python import func_without_stub as f; f', func_without_stub_sig), + ('from with_python import func_without_stub; func_without_stub', func_without_stub_sig), + + ('import with_python; with_python.func_with_stub', func_with_stub_doc), + ('import with_python.module; with_python.module.func_with_stub', func_with_stub_doc), + ('from with_python import module; module.func_with_stub', func_with_stub_doc), + ('from with_python.module import func_with_stub', func_with_stub_doc), + ('from with_python.module import func_with_stub as f; f', func_with_stub_doc), + ('from with_python.module import func_with_stub; func_with_stub', func_with_stub_doc), + ('from with_python import func_with_stub', func_with_stub_doc), + ('from with_python import func_with_stub as f; f', func_with_stub_doc), + ('from with_python import func_with_stub; func_with_stub', func_with_stub_doc), + ] +) +def test_docstrings(ScriptInStubFolder, code, expected): + d, = ScriptInStubFolder(code).help() + assert d.docstring() == expected