diff --git a/rope/contrib/autoimport/defs.py b/rope/contrib/autoimport/defs.py index b5559221c..b5d7d4841 100644 --- a/rope/contrib/autoimport/defs.py +++ b/rope/contrib/autoimport/defs.py @@ -24,6 +24,7 @@ class ModuleInfo(NamedTuple): modname: str underlined: bool process_imports: bool + description: str class ModuleFile(ModuleInfo): @@ -100,6 +101,8 @@ class Name(NamedTuple): package: str source: Source name_type: NameType + description: str + mod_desc: str class PartialName(NamedTuple): @@ -107,6 +110,8 @@ class PartialName(NamedTuple): name: str name_type: NameType + description: str + mod_desc: str class SearchResult(NamedTuple): @@ -114,5 +119,7 @@ class SearchResult(NamedTuple): import_statement: str name: str + modname: str source: int itemkind: int + description: str diff --git a/rope/contrib/autoimport/models.py b/rope/contrib/autoimport/models.py index d0acdca48..c3a295582 100644 --- a/rope/contrib/autoimport/models.py +++ b/rope/contrib/autoimport/models.py @@ -96,6 +96,8 @@ class Name(Model): "package": "TEXT", "source": "INTEGER", "type": "INTEGER", + "description": "TEXT", + "mod_desc": "TEXT", } columns = list(schema.keys()) objects = Query(table_name, columns) diff --git a/rope/contrib/autoimport/parse.py b/rope/contrib/autoimport/parse.py index 58ee8fcbd..8295ac21f 100644 --- a/rope/contrib/autoimport/parse.py +++ b/rope/contrib/autoimport/parse.py @@ -49,24 +49,38 @@ def get_names_from_file( except SyntaxError as error: print(error) return + mod_desc: str = ast.get_docstring(root_node) or "" for node in ast.iter_child_nodes(root_node): if isinstance(node, ast.Assign): for target in node.targets: try: assert isinstance(target, ast.Name) if underlined or not target.id.startswith("_"): + try: + doc = ast.get_docstring(node.value, clean=True) + except: + logger.debug( + "failed to get doc for %r, a value for %r", + node.value, + target.id, + ) + doc = "" yield PartialName( target.id, get_type_ast(node), + doc, + mod_desc, ) except (AttributeError, AssertionError): # TODO handle tuple assignment pass - elif isinstance(node, (ast.FunctionDef, ast.ClassDef)): + elif isinstance(node, (ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef)): if underlined or not node.name.startswith("_"): yield PartialName( node.name, get_type_ast(node), + ast.get_docstring(node, clean=True) or "", + mod_desc, ) elif process_imports and isinstance(node, ast.ImportFrom): # When we process imports, we want to include names in it's own package. @@ -83,7 +97,12 @@ def get_names_from_file( else: real_name = name if underlined or not real_name.startswith("_"): - yield PartialName(real_name, get_type_ast(node)) + yield PartialName( + real_name, + get_type_ast(node), + "", + mod_desc, + ) def get_type_object(imported_object) -> NameType: @@ -143,6 +162,7 @@ def get_names_from_compiled( logger.error(f"{package} could not be imported for autoimport analysis") return else: + mod_desc: str = inspect.getdoc(module) or "" for name, value in inspect.getmembers(module): if underlined or not name.startswith("_"): if ( @@ -151,10 +171,24 @@ def get_names_from_compiled( or inspect.isbuiltin(value) ): yield Name( - str(name), package, package, source, get_type_object(value) + str(name), + package, + package, + source, + get_type_object(value), + inspect.getdoc(value) or "", + mod_desc, ) def combine(package: Package, module: ModuleFile, name: PartialName) -> Name: """Combine information to form a full name.""" - return Name(name.name, module.modname, package.name, package.source, name.name_type) + return Name( + name.name, + module.modname, + package.name, + package.source, + name.name_type, + name.description, + name.mod_desc, + ) diff --git a/rope/contrib/autoimport/sqlite.py b/rope/contrib/autoimport/sqlite.py index 3d5d35977..1196613bd 100644 --- a/rope/contrib/autoimport/sqlite.py +++ b/rope/contrib/autoimport/sqlite.py @@ -234,10 +234,8 @@ def search(self, name: str, exact_match: bool = False) -> List[Tuple[str, str]]: Returns a sorted list of import statement, modname pairs """ results: List[Tuple[str, str, int]] = [ - (statement, import_name, source) - for statement, import_name, source, type in self.search_full( - name, exact_match - ) + (suggestion.import_statement, suggestion.name, suggestion.source) + for suggestion in self.search_full(name, exact_match) ] return sort_and_deduplicate_tuple(results) @@ -282,16 +280,26 @@ def _search_name( """ if not exact_match: name = name + "%" # Makes the query a starts_with query - for import_name, module, source, name_type in self._execute( - models.Name.search_by_name_like.select("name", "module", "source", "type"), + for ( + import_name, + module, + source, + name_type, + description, + ) in self._execute( + models.Name.search_by_name_like.select( + "name", "module", "source", "type", "description" + ), (name,), ): yield ( SearchResult( f"from {module} import {import_name}", import_name, + module, source, name_type, + description, ) ) @@ -305,8 +313,9 @@ def _search_module( """ if not exact_match: name = name + "%" # Makes the query a starts_with query - for module, source in self._execute( - models.Name.search_submodule_like.select("module", "source"), (name,) + for module, source, mod_desc in self._execute( + models.Name.search_submodule_like.select("module", "source", "mod_desc"), + (name,), ): parts = module.split(".") import_name = parts[-1] @@ -318,17 +327,25 @@ def _search_module( SearchResult( f"from {remaining} import {import_name}", import_name, + module, source, NameType.Module.value, + mod_desc, ) ) - for module, source in self._execute( - models.Name.search_module_like.select("module", "source"), (name,) + for module, source, mod_desc in self._execute( + models.Name.search_module_like.select("module", "source", "mod_desc"), + (name,), ): if "." in module: continue yield SearchResult( - f"import {module}", module, source, NameType.Module.value + f"import {module}", + module, + module, + source, + NameType.Module.value, + mod_desc, ) def get_modules(self, name) -> List[str]: @@ -593,6 +610,8 @@ def _convert_name(name: Name) -> tuple: name.package, name.source.value, name.name_type.value, + name.description, + name.mod_desc, ) def _add_names(self, names: Iterable[Name]): @@ -636,6 +655,7 @@ def _resource_to_module( resource_modname, underlined, resource_path.name == "__init__.py", + "", ) def _execute(self, query: models.FinalQuery, *args, **kwargs): diff --git a/rope/contrib/autoimport/utils.py b/rope/contrib/autoimport/utils.py index 5238339de..82f75b149 100644 --- a/rope/contrib/autoimport/utils.py +++ b/rope/contrib/autoimport/utils.py @@ -111,11 +111,11 @@ def get_files( """Find all files to parse in a given path using __init__.py.""" if package.type in (PackageType.COMPILED, PackageType.BUILTIN): if package.source in (Source.STANDARD, Source.BUILTIN): - yield ModuleCompiled(None, package.name, underlined, True) + yield ModuleCompiled(None, package.name, underlined, True, "") elif package.type == PackageType.SINGLE_FILE: assert package.path assert package.path.suffix == ".py" - yield ModuleFile(package.path, package.path.stem, underlined, False) + yield ModuleFile(package.path, package.path.stem, underlined, False, "") else: assert package.path for file in package.path.glob("**/*.py"): @@ -125,8 +125,13 @@ def get_files( get_modname_from_path(file.parent, package.path), underlined, True, + "", ) elif should_parse(file, underlined): yield ModuleFile( - file, get_modname_from_path(file, package.path), underlined, False + file, + get_modname_from_path(file, package.path), + underlined, + False, + "", ) diff --git a/ropetest/contrib/autoimport/parsetest.py b/ropetest/contrib/autoimport/parsetest.py index 2f59c91d3..55b1d0e8d 100644 --- a/ropetest/contrib/autoimport/parsetest.py +++ b/ropetest/contrib/autoimport/parsetest.py @@ -1,17 +1,61 @@ +import os +import pathlib +import sys +import typing +from inspect import getdoc +from os import _exit +from sys import exit + from rope.contrib.autoimport import parse from rope.contrib.autoimport.defs import Name, NameType, PartialName, Source def test_typing_names(typing_path): names = list(parse.get_names_from_file(typing_path)) - assert PartialName("Text", NameType.Variable) in names + # No docs for typing, its docstrings are SUPER weird + assert PartialName("Text", NameType.Variable, "", getdoc(typing)) in names + + +def test_docstring(): + names = list( + node + for node in parse.get_names_from_file(pathlib.Path(pathlib.__file__)) + if node.name == "Path" + ) + assert ( + PartialName("Path", NameType.Class, getdoc(pathlib.Path), getdoc(pathlib) or "") + in names + ) def test_find_sys(): names = list(parse.get_names_from_compiled("sys", Source.BUILTIN)) - assert Name("exit", "sys", "sys", Source.BUILTIN, NameType.Function) in names + print(names) + assert ( + Name( + "exit", + "sys", + "sys", + Source.BUILTIN, + NameType.Function, + getdoc(exit), + getdoc(sys), + ) + in names + ) def test_find_underlined(): names = list(parse.get_names_from_compiled("os", Source.BUILTIN, underlined=True)) - assert Name("_exit", "os", "os", Source.BUILTIN, NameType.Function) in names + assert ( + Name( + "_exit", + "os", + "os", + Source.BUILTIN, + NameType.Function, + getdoc(_exit), + getdoc(os), + ) + in names + ) diff --git a/ropetest/reprtest.py b/ropetest/reprtest.py index 96954846c..92e3a48ad 100644 --- a/ropetest/reprtest.py +++ b/ropetest/reprtest.py @@ -154,7 +154,7 @@ def test_repr_findit_location(project, mod1): def test_autoimport_models_query(project, mod1): - expected_repr = '''Query("names WHERE module LIKE (?)", columns=['name', 'module', 'package', 'source', 'type'])''' + expected_repr = '''Query("names WHERE module LIKE (?)", columns=['name', 'module', 'package', 'source', 'type', 'description', 'mod_desc'])''' obj = models.Name.search_module_like assert isinstance(obj, models.Query) assert repr(obj) == expected_repr