diff --git a/CHANGELOG.md b/CHANGELOG.md index 32a245852..e36916984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - #895 patchedast Starred and keyword now consumes their syntactically expected and ** (@lieryan) - #896 patchedast cleanup and refactoring (@lieryan) - #905 Ignore `node_modules` directories by default when scanning project files and source folders. (@marcoeangeli) +- #933 Add type annotations to project.py (@lieryan) # Release 1.15.1 diff --git a/rope/base/project.py b/rope/base/project.py index ea19722ac..c7f6a2f8e 100644 --- a/rope/base/project.py +++ b/rope/base/project.py @@ -5,8 +5,9 @@ import os import sys import warnings +from abc import ABC, abstractmethod from contextlib import ExitStack -from typing import Optional +from typing import TYPE_CHECKING, Optional, cast import rope.base.fscommands # Use full qualification for clarity. import rope.base.resourceobserver as resourceobserver @@ -16,7 +17,7 @@ # At present rope.base.prefs starts with `# type:ignore`. # As a result, mypy knows nothing about Prefs and get_config. from rope.base.prefs import Prefs, get_config # type: ignore -from rope.base.resources import File, Folder, _ResourceMatcher +from rope.base.resources import File, Folder, Resource, _ResourceMatcher try: import cPickle as pickle # type: ignore @@ -24,8 +25,14 @@ import pickle # type: ignore -class _Project: +if TYPE_CHECKING: + from rope.base import pyobjectsdef + + +class _Project(ABC): prefs: Prefs + data_files: _DataFiles + _custom_source_folders: list[Folder] def __init__(self, fscommands): self.observers = [] @@ -34,7 +41,7 @@ def __init__(self, fscommands): self.data_files = _DataFiles(self) self._custom_source_folders = [] - def get_resource(self, resource_name): + def get_resource(self, resource_name: str) -> Resource: """Get a resource in a project. `resource_name` is the path of a resource in a project. It is @@ -57,36 +64,17 @@ def get_resource(self, resource_name): else: raise exceptions.ResourceNotFoundError("Unknown resource " + resource_name) - def get_module(self, name, folder=None): - """Returns a `PyObject` if the module was found.""" - # check if this is a builtin module - pymod = self.pycore.builtin_module(name) - if pymod is not None: - return pymod - module = self.find_module(name, folder) - if module is None: - raise ModuleNotFoundError("Module %s not found" % name) - return self.pycore.resource_to_pyobject(module) - - def get_python_path_folders(self): + def get_python_path_folders(self) -> list[Folder]: result = [] for src in self.prefs.get("python_path", []) + sys.path: with contextlib.suppress(exceptions.ResourceNotFoundError): src_folder = get_no_project().get_resource(src) - result.append(src_folder) - return result - # INFO: It was decided not to cache source folders, since: - # - Does not take much time when the root folder contains - # packages, that is most of the time - # - We need a separate resource observer; `self.observer` - # does not get notified about module and folder creations - def get_source_folders(self): - """Returns project source folders""" - if self.root is None: - return [] - result = list(self._custom_source_folders) - result.extend(self.pycore._find_source_folders(self.root)) + # FIXME: it shouldn't be supported, but what if someone + # provided a .py file in python_path? + src_folder = cast(Folder, src_folder) + + result.append(src_folder) return result def validate(self, folder): @@ -125,43 +113,34 @@ def get_pymodule(self, resource, force_errors=False): def get_pycore(self): return self.pycore - def get_file(self, path): + def get_file(self, path: str) -> File: """Get the file with `path` (it may not exist)""" return File(self, path) - def get_folder(self, path): + def get_folder(self, path: str) -> Folder: """Get the folder with `path` (it may not exist)""" return Folder(self, path) def get_prefs(self): return self.prefs - def get_relative_module(self, name, folder, level): + def get_relative_module( + self, + name: str, + folder: Folder, + level: int, + ) -> pyobjectsdef.PyPackage | pyobjectsdef.PyModule: module = self.find_relative_module(name, folder, level) if module is None: raise ModuleNotFoundError("Module %s not found" % name) return self.pycore.resource_to_pyobject(module) - def find_module(self, modname, folder=None) -> Optional[File]: - """Returns a resource corresponding to the given module - - returns None if it can not be found - """ - for src in self.get_source_folders(): - module = _find_module_in_folder(src, modname) - if module is not None: - return module - for src in self.get_python_path_folders(): - module = _find_module_in_folder(src, modname) - if module is not None: - return module - if folder is not None: - module = _find_module_in_folder(folder, modname) - if module is not None: - return module - return None - - def find_relative_module(self, modname, folder, level): + def find_relative_module( + self, + modname: str, + folder: Folder, + level: int, + ) -> Optional[Resource]: for i in range(level - 1): folder = folder.parent if modname == "": @@ -172,9 +151,6 @@ def find_relative_module(self, modname, folder, level): def is_ignored(self, resource): return False - def _get_resource_path(self, name): - pass - @property @utils.saveit def history(self): @@ -182,18 +158,36 @@ def history(self): @property @utils.saveit - def pycore(self): + def pycore(self) -> pycore.PyCore: return pycore.PyCore(self) + @abstractmethod def close(self): - warnings.warn("Cannot close a NoProject", DeprecationWarning, stacklevel=2) + """Closes project open resources""" - ropefolder = None + @property + @abstractmethod + def ropefolder(self) -> Optional[Folder]: + # FIXME: use class-level ReadOnly attribute when they become available (PEP-767) + ... + + @abstractmethod + def _get_resource_path(self, name: str) -> str: ... + + @abstractmethod + def get_files(self) -> list[Resource]: + return [] + + @abstractmethod + def get_python_files(self) -> list[Resource]: + return [] class Project(_Project): """A Project containing files and folders""" + file_list: _FileListCacher + def __init__( self, projectroot, fscommands=None, ropefolder=".ropeproject", **prefs ): @@ -242,10 +236,61 @@ def _init_source_folders(self): folder = self.get_resource(path) self._custom_source_folders.append(folder) - def get_files(self): - return self.file_list.get_files() + # INFO: It was decided not to cache source folders, since: + # - Does not take much time when the root folder contains + # packages, that is most of the time + # - We need a separate resource observer; `self.observer` + # does not get notified about module and folder creations + def get_source_folders(self) -> list[Folder]: + """Returns project source folders""" + if self.root is None: + return [] + result = list(self._custom_source_folders) + result.extend(self.pycore._find_source_folders(self.root)) + return result + + def find_module( + self, + modname: str, + folder: Optional[Folder] = None, + ) -> Optional[Resource]: + """Returns a resource corresponding to the given module - def get_python_files(self): + returns None if it can not be found + """ + for src in self.get_source_folders(): + module = _find_module_in_folder(src, modname) + if module is not None: + return module + for src in self.get_python_path_folders(): + module = _find_module_in_folder(src, modname) + if module is not None: + return module + if folder is not None: + module = _find_module_in_folder(folder, modname) + if module is not None: + return module + return None + + def get_module( + self, + name: str, + folder: Optional[Folder] = None, + ) -> pyobjectsdef.PyPackage | pyobjectsdef.PyModule: + """Returns a `PyObject` if the module was found.""" + # check if this is a builtin module + pymod = self.pycore.builtin_module(name) + if pymod is not None: + return pymod + module = self.find_module(name, folder) + if module is None: + raise ModuleNotFoundError("Module %s not found" % name) + return self.pycore.resource_to_pyobject(module) + + def get_files(self) -> list[Resource]: + return list(self.file_list.get_files()) + + def get_python_files(self) -> list[Resource]: """Returns all python files available in the project""" return [ resource @@ -253,7 +298,7 @@ def get_python_files(self): if self.pycore.is_python_file(resource) ] - def _get_resource_path(self, name): + def _get_resource_path(self, name: str) -> str: return os.path.join(self._address, *name.split("/")) def _init_ropefolder(self): @@ -297,9 +342,10 @@ def set(self, key, value): self.prefs.set(key, value) @property - def ropefolder(self): + def ropefolder(self) -> Optional[Folder]: if self._ropefolder_name is not None: return self.get_folder(self._ropefolder_name) + return None def validate(self, folder=None): if folder is None: @@ -311,39 +357,45 @@ def validate(self, folder=None): class NoProject(_Project): - """A null object for holding out of project files. + """A null object for holding out-of-project files. - This class is singleton use `get_no_project` global function + This class is a singleton, use `get_no_project` global function. """ + _no_project: Optional[NoProject] = None + ropefolder: Optional[Folder] = None + def __init__(self): fscommands = rope.base.fscommands.FileSystemCommands() super().__init__(fscommands) - def _get_resource_path(self, name): + def _get_resource_path(self, name: str) -> str: real_name = name.replace("/", os.path.sep) return _realpath(real_name) - def get_resource(self, name): + def get_resource(self, name: str) -> Resource: universal_name = _realpath(name).replace(os.path.sep, "/") return super().get_resource(universal_name) - def get_files(self): + def get_files(self) -> list[Resource]: return [] - def get_python_files(self): + def get_python_files(self) -> list[Resource]: return [] - _no_project = None + def close(self): + warnings.warn("Cannot close a NoProject", DeprecationWarning, stacklevel=2) -def get_no_project(): +def get_no_project() -> NoProject: if NoProject._no_project is None: NoProject._no_project = NoProject() return NoProject._no_project class _FileListCacher: + files: set[Resource] + def __init__(self, project): self.project = project self.files = None @@ -352,7 +404,7 @@ def __init__(self, project): ) self.project.add_observer(rawobserver) - def get_files(self): + def get_files(self) -> set[Resource]: if self.files is None: self.files = set() self._add_files(self.project.root) @@ -416,7 +468,7 @@ def _get_file(self, name): return self.project.get_file(path) -def _realpath(path): +def _realpath(path: str) -> str: """Return the real path of `path` Is equivalent to ``realpath(abspath(expanduser(path)))``. @@ -437,7 +489,7 @@ def _realpath(path): return os.path.realpath(os.path.abspath(os.path.expanduser(path))) -def _find_module_in_folder(folder, modname): +def _find_module_in_folder(folder: Folder, modname: str) -> Optional[Resource]: module = folder packages = modname.split(".") for pkg in packages[:-1]: @@ -456,3 +508,4 @@ def _find_module_in_folder(folder, modname): and not module.get_child(packages[-1] + ".py").is_folder() ): return module.get_child(packages[-1] + ".py") + return None diff --git a/rope/base/pycore.py b/rope/base/pycore.py index 4d9cd5ccf..a6d6041cd 100644 --- a/rope/base/pycore.py +++ b/rope/base/pycore.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import bisect import contextlib import difflib @@ -131,7 +133,11 @@ def get_source_folders(self): """Returns project source folders""" return self.project.get_source_folders() - def resource_to_pyobject(self, resource, force_errors=False): + def resource_to_pyobject( + self, + resource, + force_errors=False, + ) -> pyobjectsdef.PyPackage | pyobjectsdef.PyModule: return self.module_cache.get_pymodule(resource, force_errors) @utils.deprecated("Use `project.get_python_files` instead")