diff --git a/CHANGELOG.md b/CHANGELOG.md index cd0c47c1a..640fe2078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # **Upcoming release** +- #516 Autoimport Now automatically detects project dependencies and can read TOML configuration +- #515 Autoimport now gathers docstrings of imported names and modules (@bagel897). - #733 skip directories with perm error when building autoimport index (@MrBago) - #722, #723 Remove site-packages from packages search tree (@tkrabel) - #738 Implement os.PathLike on Resource (@lieryan) @@ -25,10 +27,10 @@ # Release 1.8.0 - #650 Install pre-commit hooks on rope repository (@lieryan) -- #655 Remove unused __init__() methods (@edreamleo, @lieryan) +- #655 Remove unused **init**() methods (@edreamleo, @lieryan) - #656 Reformat using black 23.1.0 (@edreamleo) - #674 Fix/supress all mypy complaints (@edreamleo) -- #680 Remove a do-nothing statement in soi._handle_first_parameter (@edreamleo) +- #680 Remove a do-nothing statement in soi.\_handle_first_parameter (@edreamleo) - #687, #688 Fix autoimport not scanning packages recursively (@lieryan) # Release 1.7.0 @@ -42,15 +44,17 @@ - #627 Fix parsing of octal literal (@lieryan) - #643, #435 Fix fstrings with mismatched parens (@apmorton) - #646 Fix renaming kwargs when refactoring from imports (@apmorton) -- #648 Remove __init__ from import statement when using sqlite autoimport (@bagel897) +- #648 Remove **init** from import statement when using sqlite autoimport (@bagel897) ## Improvements - rope.contrib.generate improvements + - #640 Remove unnecessary eval in generate.py (@edreamleo) - #641 Add type annotations for rope.contrib.generate.create_generate() (@edreamleo) - call_for_nodes() improvements + - #634 Remove call_for_nodes(recursive) argument (@edreamleo) - #642 Add comments & docstrings related to call_for_nodes (@edreamleo, @lieryan) @@ -73,7 +77,6 @@ - #620 Remove unused import in occurrences.py (@edreamleo) - #625 Remove support for deprecated ast nodes (@lieryan) - ## Tests/Dev - #626 Install pre-commit hooks on rope repository (@lieryan) @@ -84,7 +87,6 @@ - #636 Update readme to reflect 1.0 has been released. (@maxnoe) - # Release 1.6.0 ## New features & Enhancements @@ -117,7 +119,6 @@ - pynames and pyobjects - #569, #572 rename pynames to pynamesdef in pyobjectsdef.ph (@edreamleo) - # Release 1.5.1 - #531 Add alternative way to retrieve version number from pyproject.toml @@ -132,7 +133,6 @@ Date: 2022-11-23 - #522 Implement patchedast parsing of MatchMapping (@lieryan) - #514 Fix inlining dictionary with inline comment (@lieryan) - # Release 1.4.0 Date: 2022-10-22 @@ -143,7 +143,6 @@ Date: 2022-10-22 - #411, #505 Fix extracting generator without parens - #18, #510 When the function is a builtin function, the call parameter's name was sometimes incorrectly identified as an AssignedName. This led to rename refactoring incorrectly renaming these parameters. - # Release 1.3.0 Date: 2022-07-29 @@ -157,7 +156,6 @@ Date: 2022-07-29 - #501, #502 Autoimport improvements - # Release 1.2.0 Date: 2022-04-22 @@ -172,9 +170,8 @@ Date: 2022-04-22 - #479 Add ABC and type hints for TaskHandle and JobSet (@bageljrkhanofemus) - #486 Drop Python 2 support (@bageljrkhanofemus, @lieryan) -- #487 Improved value inference of __all__ declaration (@lieryan) -- #424 Add some basic __repr__ to make it easier for debugging (@lieryan) - +- #487 Improved value inference of **all** declaration (@lieryan) +- #424 Add some basic **repr** to make it easier for debugging (@lieryan) # Release 1.1.1 @@ -182,7 +179,6 @@ Date: 2022-04-22 - #476 Fix rope.contrib.autoimport package missing from release (@bageljrkhanofemus) - # Release 1.1.0 Date: 2022-05-25 @@ -202,7 +198,6 @@ Date: 2022-05-25 - The pickle-based autoimport implementation is still the default, but will be deprecated sometime in the future. - # Release 1.0.0 Date: 2022-04-08 @@ -215,7 +210,6 @@ Date: 2022-04-08 - #459 Fix bug while extracting method with augmented assignment to subscript in try block (@dryobates) - # Release 0.23.0 ## Syntax support @@ -226,7 +220,7 @@ Date: 2022-04-08 ## Bug fixes -- #134, #453 Preserve newline format when writing files (@lieryan) +- #134, #453 Preserve newline format when writing files (@lieryan) - #457 Fix extract info collection for list comprehension with multiple targets (@lieryan) @@ -234,7 +228,6 @@ Date: 2022-04-08 - #455 Fix typo (@Jasha10) - # Release 0.22.0 Date: 2021-11-23 @@ -252,7 +245,6 @@ Date: 2021-11-23 - #447 Add Python 3.10 to tests - # Release 0.21.1 Date: 2021-11-11 @@ -261,7 +253,6 @@ Date: 2021-11-11 - #441. Start publishing wheel packages to allow offline installs - # Release 0.21.0 Date: 2021-10-18 @@ -299,7 +290,6 @@ Date: 2021-10-18 - #399 Add Github Actions to enforce black code style (@lieryan) - #403 Remove plain 'unittest' only runner (@lieryan) - # Release 0.20.1 Date: 2021-09-18 @@ -309,7 +299,6 @@ Date: 2021-09-18 - Fix caller of `_namedexpr_last()` throwing exception due to returning unexpected list instead of boolean - # Release 0.20.0 Date: 2021-09-18 @@ -331,7 +320,6 @@ Date: 2021-09-18 - #380 Fix list of variables that are returned and/or turned into argument when extracting method in a loop - # Previous releases [Changelog from pre-0.10.0](https://github.com/python-rope/rope/blob/595af418e7e7e844dcce600778e1c650c2fc0ba1/docs/done.rst). diff --git a/docs/configuration.rst b/docs/configuration.rst index 5aba53acb..fb703f862 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -14,6 +14,8 @@ Will be used if [tool.rope] is configured. [tool.rope] split_imports = true + [tool.rope.autoimport] + underlined = false config.py --------- @@ -56,9 +58,14 @@ It follows the exact same syntax of the pyproject.toml. Options -------- +======= .. autopytoolconfigtable:: rope.base.prefs.Prefs +Autoimport Options +------------------ +.. autopytoolconfigtable:: rope.base.prefs.AutoimportPrefs + + Old Configuration File ---------------------- This is a sample config.py. While this config.py works and all options here should be supported, the above documentation reflects the latest version of rope. diff --git a/pyproject.toml b/pyproject.toml index f35459cfd..66b2ab3dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ 'Topic :: Software Development', ] version = '1.11.0' -dependencies = ['pytoolconfig[global] >= 1.2.2'] +dependencies = ['pytoolconfig[global] >= 1.3.0'] [[project.authors]] name = 'Ali Gholami Rudi' diff --git a/rope/base/prefs.py b/rope/base/prefs.py index 9d4fd4a43..314408fb1 100644 --- a/rope/base/prefs.py +++ b/rope/base/prefs.py @@ -2,6 +2,7 @@ # type: ignore """Rope preferences.""" from dataclasses import asdict, dataclass +from enum import Enum, unique from textwrap import dedent from typing import Any, Callable, Dict, List, Optional, Tuple @@ -10,6 +11,19 @@ from pytoolconfig.sources import Source from rope.base.resources import Folder +@unique +class DocumentationMode(Enum): + DISABLED = False + ENABLED = True + LAZY = "lazy" + +@dataclass +class AutoimportPrefs: + underlined: bool = field( + default=False, description="Cache underlined (private) modules") + memory: bool = field(default=None, description="Cache in memory instead of disk") + parallel: bool = field(default=True, description="Use multiple processes to parse") + documentation: DocumentationMode = field(default=DocumentationMode.DISABLED, description="Cache documentation") @dataclass @@ -206,6 +220,8 @@ class Prefs: Can only be set in config.py. """), ) + autoimport: AutoimportPrefs = field( + default_factory=lambda: AutoimportPrefs(), description="Preferences for Autoimport") def set(self, key: str, value: Any): """Set the value of `key` preference to `value`.""" diff --git a/rope/base/versioning.py b/rope/base/versioning.py index 985255e47..4f0615902 100644 --- a/rope/base/versioning.py +++ b/rope/base/versioning.py @@ -1,3 +1,4 @@ +import dataclasses import hashlib import importlib.util import json @@ -31,7 +32,9 @@ def _get_prefs_data(project) -> str: del prefs_data["project_opened"] del prefs_data["callbacks"] del prefs_data["dependencies"] - return json.dumps(prefs_data, sort_keys=True, indent=2) + return json.dumps( + prefs_data, sort_keys=True, indent=2, default=lambda o: o.__dict__ + ) def _get_file_content(module_name: str) -> str: 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..88e2cef3b 100644 --- a/rope/contrib/autoimport/parse.py +++ b/rope/contrib/autoimport/parse.py @@ -42,6 +42,7 @@ def get_names_from_file( package_name: str = "", underlined: bool = False, process_imports: bool = False, + get_docstring: bool = False, ) -> Generator[PartialName, None, None]: """Get all the names from a given file using ast.""" try: @@ -49,24 +50,39 @@ def get_names_from_file( except SyntaxError as error: print(error) return + mod_desc: str = ast.get_docstring(root_node) or "" if get_docstring else "" 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("_"): + doc = "" + if get_docstring: + 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, + ) yield PartialName( target.id, get_type_ast(node), + doc or "", + 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 +99,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: @@ -95,11 +116,15 @@ def get_type_object(imported_object) -> NameType: return NameType.Variable -def get_names(module: ModuleInfo, package: Package) -> List[Name]: +def get_names( + module: ModuleInfo, package: Package, get_docstring: bool = False +) -> List[Name]: """Get all names from a module and package.""" if isinstance(module, ModuleCompiled): return list( - get_names_from_compiled(package.name, package.source, module.underlined) + get_names_from_compiled( + package.name, package.source, module.underlined, get_docstring + ) ) if isinstance(module, ModuleFile): return [ @@ -118,6 +143,7 @@ def get_names_from_compiled( package: str, source: Source, underlined: bool = False, + get_docstring: bool = False, ) -> Generator[Name, None, None]: """ Get the names from a compiled module. @@ -143,6 +169,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 "" if get_docstring else "" for name, value in inspect.getmembers(module): if underlined or not name.startswith("_"): if ( @@ -151,10 +178,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 "" if get_docstring else "", + 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..8fd6ce0c7 100644 --- a/rope/contrib/autoimport/sqlite.py +++ b/rope/contrib/autoimport/sqlite.py @@ -16,7 +16,10 @@ from threading import local from typing import Generator, Iterable, Iterator, List, Optional, Set, Tuple +from packaging.requirements import Requirement + from rope.base import exceptions, libutils, resourceobserver, taskhandle, versioning +from rope.base.prefs import AutoimportPrefs, DocumentationMode from rope.base.project import Project from rope.base.resources import Resource from rope.contrib.autoimport import models @@ -41,7 +44,10 @@ def get_future_names( - packages: List[Package], underlined: bool, job_set: taskhandle.BaseJobSet + packages: List[Package], + underlined: bool, + job_set: taskhandle.BaseJobSet, + get_docstring: bool, ) -> Generator[Future, None, None]: """Get all names as futures.""" with ProcessPoolExecutor() as executor: @@ -49,22 +55,40 @@ def get_future_names( for module in get_files(package, underlined): job_set.started_job(module.modname) job_set.increment() - yield executor.submit(get_names, module, package) + yield executor.submit(get_names, module, package, get_docstring) def filter_packages( - packages: Iterable[Package], underlined: bool, existing: List[str] + packages: Iterable[Package], + underlined: bool, + existing: List[str], + dependencies: Optional[List[Requirement]], ) -> Iterable[Package]: """Filter list of packages to parse.""" + parsed_deps = ( + [dep.name for dep in dependencies] if dependencies is not None else None + ) + + def is_dep(package) -> bool: + return ( + parsed_deps is None + or package.name in parsed_deps + or package.source is not Source.SITE_PACKAGE + ) + if underlined: def filter_package(package: Package) -> bool: - return package.name not in existing + return package.name not in existing and is_dep(package) else: def filter_package(package: Package) -> bool: - return package.name not in existing and not package.name.startswith("_") + return ( + package.name not in existing + and not package.name.startswith("_") + and is_dep(package) + ) return filter(filter_package, packages) @@ -81,16 +105,15 @@ class AutoImport: """ connection: sqlite3.Connection - memory: bool project: Project project_package: Package - underlined: bool + prefs: AutoimportPrefs def __init__( self, project: Project, observe: bool = True, - underlined: bool = False, + underlined: Optional[bool] = None, memory: bool = _deprecated_default, ): """Construct an AutoImport object. @@ -113,25 +136,28 @@ def __init__( autoimport = AutoImport(..., memory=True) """ self.project = project + self.prefs = self.project.prefs.autoimport project_package = get_package_tuple(project.root.pathlib, project) assert project_package is not None assert project_package.path is not None + if underlined is not None: + self.prefs.underlined = underlined self.project_package = project_package - self.underlined = underlined - self.memory = memory if memory is _deprecated_default: - self.memory = True + self.prefs.memory = True warnings.warn( "The default value for `AutoImport(memory)` argument will " "change to use an on-disk database by default in the future. " "If you want to use an in-memory database, you need to pass " - "`AutoImport(memory=True)` explicitly.", + "`AutoImport(memory=True)` explicitly or set it in the config file.", DeprecationWarning, ) + else: + self.prefs.memory = memory self.thread_local = local() self.connection = self.create_database_connection( project=project, - memory=memory, + memory=self.prefs.memory, ) self._setup_db() if observe: @@ -187,7 +213,7 @@ def connection(self): if not hasattr(self.thread_local, "connection"): self.thread_local.connection = self.create_database_connection( project=self.project, - memory=self.memory, + memory=self.prefs.memory, ) return self.thread_local.connection @@ -234,10 +260,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 +306,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 +339,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 +353,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]: @@ -389,12 +432,13 @@ def generate_modules_cache( """ Generate global name cache for external modules listed in `modules`. - If no modules are provided, it will generate a cache for every module available. + If modules is not specified, uses PEP 621 metadata. + If modules aren't specified and PEP 621 is not present, caches every package This method searches in your sys.path and configured python folders. Do not use this for generating your own project's internal names, use generate_resource_cache for that instead. """ - underlined = self.underlined if underlined is None else underlined + underlined = self.prefs.underlined if underlined is None else underlined packages: List[Package] = ( self._get_available_packages() @@ -403,21 +447,29 @@ def generate_modules_cache( ) existing = self._get_packages_from_cache() - packages = list(filter_packages(packages, underlined, existing)) - if not packages: + packages = list( + filter_packages( + packages, underlined, existing, self.project.prefs.dependencies + ) + ) + if len(packages) == 0: return self._add_packages(packages) job_set = task_handle.create_jobset("Generating autoimport cache", 0) - if single_thread: + if single_thread or not self.prefs.parallel: for package in packages: for module in get_files(package, underlined): job_set.started_job(module.modname) - for name in get_names(module, package): + for name in get_names( + module, package, self._should_preproccess_docstring + ): self._add_name(name) job_set.finished_job() else: for future_name in as_completed( - get_future_names(packages, underlined, job_set) + get_future_names( + packages, underlined, job_set, self._should_preproccess_docstring + ) ): self._add_names(future_name.result()) job_set.finished_job() @@ -512,10 +564,12 @@ def update_resource( self, resource: Resource, underlined: bool = False, commit: bool = True ): """Update the cache for global names in `resource`.""" - underlined = underlined if underlined else self.underlined + underlined = underlined if underlined else self.prefs.underlined module = self._resource_to_module(resource, underlined) self._del_if_exist(module_name=module.modname, commit=False) - for name in get_names(module, self.project_package): + for name in get_names( + module, self.project_package, self._should_preproccess_docstring + ): self._add_name(name) if commit: self.connection.commit() @@ -537,7 +591,11 @@ def _del_if_exist(self, module_name, commit: bool = True): def _get_python_folders(self) -> List[Path]: def filter_folders(folder: Path) -> bool: - return folder.is_dir() and folder.as_posix() != "/usr/bin" + return ( + folder.is_dir() + and folder.as_posix() != "/usr/bin" + and str(folder) != self.project.address + ) folders = self.project.get_python_path_folders() folder_paths = filter(filter_folders, map(Path, folders)) @@ -593,6 +651,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]): @@ -623,7 +683,7 @@ def _resource_to_module( self, resource: Resource, underlined: bool = False ) -> ModuleFile: assert self.project_package.path - underlined = underlined if underlined else self.underlined + underlined = underlined if underlined else self.prefs.underlined resource_path: Path = resource.pathlib # The project doesn't need its name added to the path, # since the standard python file layout accounts for that @@ -636,6 +696,7 @@ def _resource_to_module( resource_modname, underlined, resource_path.name == "__init__.py", + "", ) def _execute(self, query: models.FinalQuery, *args, **kwargs): @@ -645,3 +706,7 @@ def _execute(self, query: models.FinalQuery, *args, **kwargs): def _executemany(self, query: models.FinalQuery, *args, **kwargs): assert isinstance(query, models.FinalQuery) return self.connection.executemany(query._query, *args, **kwargs) + + @property + def _should_preproccess_docstring(self) -> bool: + return self.prefs.documentation is DocumentationMode.ENABLED diff --git a/rope/contrib/autoimport/utils.py b/rope/contrib/autoimport/utils.py index 5238339de..9229ea6f8 100644 --- a/rope/contrib/autoimport/utils.py +++ b/rope/contrib/autoimport/utils.py @@ -111,22 +111,29 @@ 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"): + for file in package.path.rglob("*.py"): + if "site-packages" in file.relative_to(package.path).parts: + continue if file.name == "__init__.py": yield ModuleFile( file, 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/autoimporttest.py b/ropetest/contrib/autoimport/autoimporttest.py index 072ab1ed8..f9496db60 100644 --- a/ropetest/contrib/autoimport/autoimporttest.py +++ b/ropetest/contrib/autoimport/autoimporttest.py @@ -14,7 +14,7 @@ @pytest.fixture def autoimport(project: Project): - with closing(AutoImport(project)) as ai: + with closing(AutoImport(project, memory=True)) as ai: yield ai @@ -124,9 +124,9 @@ def foo(): def test_connection(project: Project, project2: Project): - ai1 = AutoImport(project) - ai2 = AutoImport(project) - ai3 = AutoImport(project2) + ai1 = AutoImport(project, memory=True) + ai2 = AutoImport(project, memory=True) + ai3 = AutoImport(project2, memory=True) assert ai1.connection is not ai2.connection assert ai1.connection is not ai3.connection diff --git a/ropetest/contrib/autoimport/deptest.py b/ropetest/contrib/autoimport/deptest.py new file mode 100644 index 000000000..9dcefd132 --- /dev/null +++ b/ropetest/contrib/autoimport/deptest.py @@ -0,0 +1,54 @@ +from pathlib import Path +from typing import Iterable + +import pytest + +from rope.base.project import Project +from rope.contrib.autoimport.sqlite import AutoImport + + +@pytest.fixture +def project(request, tmp_path: Path) -> Iterable[Project]: + doc = request.param + if doc is not None: + file = tmp_path / "pyproject.toml" + file.write_text(doc, encoding="utf-8") + print(file, doc) + project = Project(tmp_path) + yield project + project.close() + + +@pytest.fixture +def autoimport(project) -> Iterable[AutoImport]: + autoimport = AutoImport(project, memory=True) + autoimport.generate_modules_cache() + yield autoimport + autoimport.close() + + +@pytest.mark.parametrize("project", ((""),), indirect=True) +def test_blank(project, autoimport): + assert project.prefs.dependencies is None + assert autoimport.search("pytoolconfig") + + +@pytest.mark.parametrize("project", (("[project]\n dependencies=[]"),), indirect=True) +def test_empty(project, autoimport): + assert len(project.prefs.dependencies) == 0 + assert [] == autoimport.search("pytoolconfig") + + +FILE = """ +[project] +dependencies = [ + "pytoolconfig", + "bogus" +] +""" + + +@pytest.mark.parametrize("project", ((FILE),), indirect=True) +def test_not_empty(project, autoimport): + assert len(project.prefs.dependencies) == 2 + assert autoimport.search("pytoolconfig") 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/contrib/autoimporttest.py b/ropetest/contrib/autoimporttest.py index 94bb033bb..fb21bbd2b 100644 --- a/ropetest/contrib/autoimporttest.py +++ b/ropetest/contrib/autoimporttest.py @@ -11,7 +11,7 @@ def setUp(self): self.mod1 = testutils.create_module(self.project, "mod1") self.pkg = testutils.create_package(self.project, "pkg") self.mod2 = testutils.create_module(self.project, "mod2", self.pkg) - self.importer = autoimport.AutoImport(self.project, observe=False) + self.importer = autoimport.AutoImport(self.project, observe=False, memory=True) def tearDown(self): testutils.remove_project(self.project) @@ -86,7 +86,7 @@ def test_not_caching_underlined_names(self): self.assertEqual(["mod1"], self.importer.get_modules("_myvar")) def test_caching_underlined_names_passing_to_the_constructor(self): - importer = autoimport.AutoImport(self.project, False, True) + importer = autoimport.AutoImport(self.project, False, True, memory=True) self.mod1.write("_myvar = None\n") importer.update_resource(self.mod1) self.assertEqual(["mod1"], importer.get_modules("_myvar")) @@ -165,7 +165,7 @@ def setUp(self): self.mod1 = testutils.create_module(self.project, "mod1") self.pkg = testutils.create_package(self.project, "pkg") self.mod2 = testutils.create_module(self.project, "mod2", self.pkg) - self.importer = autoimport.AutoImport(self.project, observe=True) + self.importer = autoimport.AutoImport(self.project, observe=True, memory=True) def tearDown(self): testutils.remove_project(self.project) 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