Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
201 changes: 127 additions & 74 deletions rope/base/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -16,16 +17,22 @@
# 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
except ImportError:
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 = []
Expand All @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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 == "":
Expand All @@ -172,28 +151,43 @@ 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):
return history.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
):
Expand Down Expand Up @@ -242,18 +236,69 @@ 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
for resource in self.get_files()
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):
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)))``.
Expand All @@ -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]:
Expand All @@ -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
8 changes: 7 additions & 1 deletion rope/base/pycore.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import bisect
import contextlib
import difflib
Expand Down Expand Up @@ -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")
Expand Down
Loading