diff --git a/.gitignore b/.gitignore index 15b8071ce..ce320e7ea 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ __pycache__/ .Python build/ develop-eggs/ -dist/ +/dist/ downloads/ eggs/ .eggs/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 875269763..d24a9b8e1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,20 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-added-large-files - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 24.1.1 hooks: - id: black name: black check (Python) - repo: https://github.com/pycqa/isort - rev: 5.11.3 + rev: 5.13.2 hooks: - id: isort name: Sort imports (Python) diff --git a/CHANGELOG.md b/CHANGELOG.md index 092d5fbaf..cfa7baf67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # **Upcoming release** - Check for ast.Attributes when finding occurrences in fstrings (@sandratsy) +- #777, #698 add validation to refuse Rename refactoring to a python keyword +- #730 Match on module aliases for autoimport suggestions +- #755 Remove dependency on `build` package being installed while running tests # Release 1.12.0 diff --git a/MANIFEST.in b/MANIFEST.in index 86e75b337..c6e77775f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include README.rst COPYING setup.py MANIFEST.in CHANGELOG.md +include README.rst COPYING setup.py MANIFEST.in CHANGELOG.md ropetest-package-fixtures/external_fixturepkg/dist/external_fixturepkg-1.0.0-py3-none-any.whl ropetest-package-fixtures/external_fixturepkg/dist/external_fixturepkg-1.0.0.tar.gz recursive-include rope *.py recursive-include docs *.rst recursive-include ropetest *.py diff --git a/docs/conf.py b/docs/conf.py index d50a9cbcf..9313fcb16 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,6 +12,7 @@ # import os import sys + sys.path.insert(0, os.path.abspath("..")) diff --git a/docs/configuration.rst b/docs/configuration.rst index 5aba53acb..b30bc314f 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -14,6 +14,11 @@ Will be used if [tool.rope] is configured. [tool.rope] split_imports = true + autoimport.aliases = [ + ['dt', 'datetime'], + ['mp', 'multiprocessing'], + ] + config.py --------- @@ -48,9 +53,9 @@ Additionally, you can run an executable function at startup of rope. pytool.toml ----------- If neither a config.py or a pyproject.toml is present, rope will use a pytool.toml. -It follows the exact same syntax of the pyproject.toml. +It follows the exact same syntax as ``pyproject.toml``. -- Mac OS X: ``~/Library/Application Support/pytool.toml.`` +- Mac OS X: ``~/Library/Application Support/pytool.toml``. - Unix: ``~/.config/pytool.toml``` or in $XDG_CONFIG_HOME, if defined - Windows: ``C:\Users\\AppData\Local\pytool.toml`` diff --git a/pyproject.toml b/pyproject.toml index 0df77c259..276938f34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,7 @@ profile = "black" [tool.pytest.ini_options] +testpaths = "ropetest" python_files = [ "*test.py", "__init__.py", diff --git a/rope/base/builtins.py b/rope/base/builtins.py index 151c64742..9c8bd1611 100644 --- a/rope/base/builtins.py +++ b/rope/base/builtins.py @@ -1,4 +1,5 @@ """This module tries to support builtin types and functions.""" + import inspect import io diff --git a/rope/base/fscommands.py b/rope/base/fscommands.py index 00ba71c8a..a6104448c 100644 --- a/rope/base/fscommands.py +++ b/rope/base/fscommands.py @@ -6,6 +6,7 @@ `MercurialCommands` for example. """ + import os import re import shutil diff --git a/rope/base/libutils.py b/rope/base/libutils.py index f5b0ff461..0974b11e7 100644 --- a/rope/base/libutils.py +++ b/rope/base/libutils.py @@ -1,4 +1,5 @@ """A few useful functions for using rope as a library""" + import os.path import rope.base.project diff --git a/rope/base/oi/soi.py b/rope/base/oi/soi.py index 8bdadaa1c..090b10337 100644 --- a/rope/base/oi/soi.py +++ b/rope/base/oi/soi.py @@ -4,6 +4,7 @@ package. """ + import rope.base.builtins # Use full qualification for clarity. from rope.base import arguments, evaluate, pynames, pyobjects, utils from rope.base.oi.type_hinting.factory import get_type_hinting_factory diff --git a/rope/base/oi/transform.py b/rope/base/oi/transform.py index 968e70b25..ace4261f3 100644 --- a/rope/base/oi/transform.py +++ b/rope/base/oi/transform.py @@ -1,4 +1,5 @@ """Provides classes for persisting `PyObject`""" + import os import re diff --git a/rope/base/oi/type_hinting/providers/docstrings.py b/rope/base/oi/type_hinting/providers/docstrings.py index d50d2fd67..3ece79688 100644 --- a/rope/base/oi/type_hinting/providers/docstrings.py +++ b/rope/base/oi/type_hinting/providers/docstrings.py @@ -21,6 +21,7 @@ - https://groups.google.com/d/topic/rope-dev/LCFNN98vckI/discussion """ + import re from rope.base.oi.type_hinting import utils diff --git a/rope/base/oi/type_hinting/providers/numpydocstrings.py b/rope/base/oi/type_hinting/providers/numpydocstrings.py index 35788f797..1bd15b989 100644 --- a/rope/base/oi/type_hinting/providers/numpydocstrings.py +++ b/rope/base/oi/type_hinting/providers/numpydocstrings.py @@ -3,6 +3,7 @@ https://github.com/davidhalter/jedi/blob/b489019f5bd5750051122b94cc767df47751ecb7/jedi/evaluate/docstrings.py Thanks to @davidhalter for this utils under MIT License. """ + import re from rope.base.ast import literal_eval diff --git a/rope/base/prefs.py b/rope/base/prefs.py index 9d4fd4a43..2e698eba1 100644 --- a/rope/base/prefs.py +++ b/rope/base/prefs.py @@ -12,6 +12,30 @@ from rope.base.resources import Folder +@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") + + aliases: List[Tuple[str, str]] = field( + default_factory=lambda : [ + ("np", "numpy"), + ("pd", "pandas"), + ("plt", "matplotlib.pyplot"), + ("sns", "seaborn"), + ("tf", "tensorflow"), + ("sk", "sklearn"), + ("sm", "statsmodels"), + ], + description=dedent(""" + Aliases for module names. For example, `[('np', 'numpy')]` makes rope recommend + ``import numpy as np``. + """), + ) + + @dataclass class Prefs: """Class to store rope preferences.""" @@ -139,7 +163,6 @@ class Prefs: appear in the importing namespace. """), ) - prefer_module_from_imports: bool = field( default=False, description=dedent(""" @@ -206,6 +229,8 @@ class Prefs: Can only be set in config.py. """), ) + autoimport: AutoimportPrefs = field( + default_factory=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/project.py b/rope/base/project.py index 067a74037..84ea237d1 100644 --- a/rope/base/project.py +++ b/rope/base/project.py @@ -1,4 +1,5 @@ from __future__ import annotations + import contextlib import json import os diff --git a/rope/base/simplify.py b/rope/base/simplify.py index ac5acce26..3198c26de 100644 --- a/rope/base/simplify.py +++ b/rope/base/simplify.py @@ -2,6 +2,7 @@ This module is here to help source code analysis. """ + import re from rope.base import codeanalyze, utils 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/__init__.py b/rope/contrib/autoimport/__init__.py index 65459cef4..a151465f2 100644 --- a/rope/contrib/autoimport/__init__.py +++ b/rope/contrib/autoimport/__init__.py @@ -1,4 +1,5 @@ """AutoImport module for rope.""" + from .pickle import AutoImport as _PickleAutoImport from .sqlite import AutoImport as _SqliteAutoImport diff --git a/rope/contrib/autoimport/defs.py b/rope/contrib/autoimport/defs.py index b5559221c..3078f7d65 100644 --- a/rope/contrib/autoimport/defs.py +++ b/rope/contrib/autoimport/defs.py @@ -1,4 +1,5 @@ """Definitions of types for the Autoimport program.""" + import pathlib from enum import Enum from typing import NamedTuple, Optional @@ -92,6 +93,13 @@ class Package(NamedTuple): type: PackageType +class Alias(NamedTuple): + """A module alias to be added to the database.""" + + alias: str + modname: str + + class Name(NamedTuple): """A Name to be added to the database.""" diff --git a/rope/contrib/autoimport/models.py b/rope/contrib/autoimport/models.py index d0acdca48..1fd1684e3 100644 --- a/rope/contrib/autoimport/models.py +++ b/rope/contrib/autoimport/models.py @@ -56,13 +56,11 @@ def delete_from(self) -> FinalQuery: class Model(ABC): @property @abstractmethod - def table_name(self) -> str: - ... + def table_name(self) -> str: ... @property @abstractmethod - def schema(self) -> Dict[str, str]: - ... + def schema(self) -> Dict[str, str]: ... @classmethod def create_table(cls, connection): @@ -88,6 +86,29 @@ class Metadata(Model): objects = Query(table_name, columns) +class Alias(Model): + table_name = "aliases" + schema = { + "alias": "TEXT", + "module": "TEXT", + } + columns = list(schema.keys()) + objects = Query(table_name, columns) + + @classmethod + def create_table(cls, connection): + super().create_table(connection) + connection.execute( + "CREATE INDEX IF NOT EXISTS aliases_alias_nocase ON aliases(alias COLLATE NOCASE)" + ) + + modules = Query( + "(SELECT DISTINCT aliases.*, package, source, type FROM aliases INNER JOIN names on aliases.module = names.module)", + columns + ["package", "source", "type"], + ) + search_modules_with_alias = modules.where("alias LIKE (?)") + + class Name(Model): table_name = "names" schema = { diff --git a/rope/contrib/autoimport/pickle.py b/rope/contrib/autoimport/pickle.py index 431d994ee..619b40095 100644 --- a/rope/contrib/autoimport/pickle.py +++ b/rope/contrib/autoimport/pickle.py @@ -10,7 +10,6 @@ sqlite-based storage backend (rope.contrib.autoimport.sqlite.AutoImport). """ - import contextlib import re diff --git a/rope/contrib/autoimport/sqlite.py b/rope/contrib/autoimport/sqlite.py index 3d5d35977..ceee25980 100644 --- a/rope/contrib/autoimport/sqlite.py +++ b/rope/contrib/autoimport/sqlite.py @@ -2,15 +2,15 @@ import contextlib import json -from hashlib import sha256 -import secrets import re +import secrets import sqlite3 import sys import warnings from collections import OrderedDict from concurrent.futures import Future, ProcessPoolExecutor, as_completed from datetime import datetime +from hashlib import sha256 from itertools import chain from pathlib import Path from threading import local @@ -21,6 +21,7 @@ from rope.base.resources import Resource from rope.contrib.autoimport import models from rope.contrib.autoimport.defs import ( + Alias, ModuleFile, Name, NameType, @@ -330,6 +331,13 @@ def _search_module( yield SearchResult( f"import {module}", module, source, NameType.Module.value ) + for alias, module, source in self._execute( + models.Alias.search_modules_with_alias.select("alias", "module", "source"), + (name,), + ): + yield SearchResult( + f"import {module} as {alias}", alias, source, NameType.Module.value + ) def get_modules(self, name) -> List[str]: """Get the list of modules that have global `name`.""" @@ -471,11 +479,14 @@ def clear_cache(self): """ with self.connection: self._execute(models.Name.objects.drop_table()) + self._execute(models.Alias.objects.drop_table()) self._execute(models.Package.objects.drop_table()) self._execute(models.Metadata.objects.drop_table()) models.Name.create_table(self.connection) + models.Alias.create_table(self.connection) models.Package.create_table(self.connection) models.Metadata.create_table(self.connection) + self.add_aliases(self.project.prefs.autoimport.aliases) data = ( versioning.calculate_version_hash(self.project), json.dumps(versioning.get_version_hash_data(self.project)), @@ -595,6 +606,10 @@ def _convert_name(name: Name) -> tuple: name.name_type.value, ) + def add_aliases(self, aliases: Iterable[Alias]): + if aliases: + self._executemany(models.Alias.objects.insert_into(), aliases) + def _add_names(self, names: Iterable[Name]): if names is not None: self._executemany( diff --git a/rope/contrib/autoimport/utils.py b/rope/contrib/autoimport/utils.py index 5238339de..a16efba05 100644 --- a/rope/contrib/autoimport/utils.py +++ b/rope/contrib/autoimport/utils.py @@ -1,4 +1,5 @@ """Utility functions for the autoimport code.""" + import pathlib import sys from collections import OrderedDict @@ -53,7 +54,7 @@ def get_package_source( if "site-packages" in package.parts: return Source.SITE_PACKAGE if sys.version_info < (3, 10, 0): - if str(package).startswith(sys.prefix): + if str(package).startswith(sys.base_prefix): return Source.STANDARD else: if name in sys.stdlib_module_names: diff --git a/rope/contrib/finderrors.py b/rope/contrib/finderrors.py index 335e6d84a..32ee31f98 100644 --- a/rope/contrib/finderrors.py +++ b/rope/contrib/finderrors.py @@ -23,6 +23,7 @@ * ... ;-) """ + from rope.base import ast, evaluate, pyobjects diff --git a/rope/contrib/fixmodnames.py b/rope/contrib/fixmodnames.py index 493da5637..d784381ca 100644 --- a/rope/contrib/fixmodnames.py +++ b/rope/contrib/fixmodnames.py @@ -15,6 +15,7 @@ argument. """ + from rope.base import taskhandle from rope.contrib import changestack from rope.refactor import rename diff --git a/rope/refactor/__init__.py b/rope/refactor/__init__.py index 491e94237..c4beed7fc 100644 --- a/rope/refactor/__init__.py +++ b/rope/refactor/__init__.py @@ -45,6 +45,7 @@ monitoring the progress of refactorings. """ + from rope.refactor.importutils import ImportOrganizer # noqa from rope.refactor.topackage import ModuleToPackage # noqa diff --git a/rope/refactor/importutils/__init__.py b/rope/refactor/importutils/__init__.py index e13276eb1..ef0f1bac2 100644 --- a/rope/refactor/importutils/__init__.py +++ b/rope/refactor/importutils/__init__.py @@ -4,6 +4,7 @@ refactorings or as a separate task. """ + import rope.base.codeanalyze import rope.base.evaluate from rope.base import libutils diff --git a/rope/refactor/inline.py b/rope/refactor/inline.py index 23c8a8291..768d30b7d 100644 --- a/rope/refactor/inline.py +++ b/rope/refactor/inline.py @@ -406,9 +406,9 @@ def _get_definition_params(self): "Cannot inline functions with list and keyword arguments." ) if self.pyfunction.get_kind() == "classmethod": - paramdict[ - definition_info.args_with_defaults[0][0] - ] = self.pyfunction.parent.get_name() + paramdict[definition_info.args_with_defaults[0][0]] = ( + self.pyfunction.parent.get_name() + ) return paramdict def get_function_name(self): diff --git a/rope/refactor/move.py b/rope/refactor/move.py index 5916f00af..4240288d9 100644 --- a/rope/refactor/move.py +++ b/rope/refactor/move.py @@ -4,10 +4,11 @@ based on inputs. """ + from __future__ import annotations import typing -from typing import Optional, List, Union +from typing import List, Optional, Union from rope.base import ( codeanalyze, diff --git a/rope/refactor/occurrences.py b/rope/refactor/occurrences.py index 546c83a3f..b136cc5c9 100644 --- a/rope/refactor/occurrences.py +++ b/rope/refactor/occurrences.py @@ -35,7 +35,6 @@ arguments """ - import contextlib import re from typing import Iterator diff --git a/rope/refactor/rename.py b/rope/refactor/rename.py index cb6348009..fcdd17964 100644 --- a/rope/refactor/rename.py +++ b/rope/refactor/rename.py @@ -1,4 +1,5 @@ import warnings +from keyword import iskeyword from rope.base import ( codeanalyze, @@ -105,6 +106,7 @@ def unsure_func(value=unsure): resources = [self.resource] if resources is None: resources = self.project.get_python_files() + self.validate_changes(new_name) changes = ChangeSet(f"Renaming <{self.old_name}> to <{new_name}>") finder = occurrences.create_finder( self.project, @@ -128,6 +130,16 @@ def unsure_func(value=unsure): self._rename_module(resource, new_name, changes) return changes + def validate_changes( + self, + new_name: str, + **_unused, + ): + if iskeyword(new_name): + raise exceptions.RefactoringError( + f"Invalid refactoring target name. '{new_name}' is a Python keyword." + ) + def _is_allowed_to_move(self, resources, resource): if resource.is_folder(): try: diff --git a/rope/refactor/similarfinder.py b/rope/refactor/similarfinder.py index 6d2d2e1bc..dd5bec362 100644 --- a/rope/refactor/similarfinder.py +++ b/rope/refactor/similarfinder.py @@ -1,4 +1,5 @@ """This module can be used for finding similar code""" + import re import rope.base.builtins # Use full qualification for clarity. diff --git a/ropetest-package-fixtures/external_fixturepkg/README.md b/ropetest-package-fixtures/external_fixturepkg/README.md new file mode 100644 index 000000000..d6f5f60d2 --- /dev/null +++ b/ropetest-package-fixtures/external_fixturepkg/README.md @@ -0,0 +1,12 @@ +# example_project + +Just an example project for testing rope. + + +To build this package, run: + +```bash +$ python -m build +``` + +This generates packages in `dist` folder. diff --git a/ropetest-package-fixtures/external_fixturepkg/dist/external_fixturepkg-1.0.0-py3-none-any.whl b/ropetest-package-fixtures/external_fixturepkg/dist/external_fixturepkg-1.0.0-py3-none-any.whl new file mode 100644 index 000000000..ba7951d5f Binary files /dev/null and b/ropetest-package-fixtures/external_fixturepkg/dist/external_fixturepkg-1.0.0-py3-none-any.whl differ diff --git a/ropetest-package-fixtures/external_fixturepkg/dist/external_fixturepkg-1.0.0.tar.gz b/ropetest-package-fixtures/external_fixturepkg/dist/external_fixturepkg-1.0.0.tar.gz new file mode 100644 index 000000000..3ea5e8759 Binary files /dev/null and b/ropetest-package-fixtures/external_fixturepkg/dist/external_fixturepkg-1.0.0.tar.gz differ diff --git a/ropetest-package-fixtures/external_fixturepkg/pyproject.toml b/ropetest-package-fixtures/external_fixturepkg/pyproject.toml new file mode 100644 index 000000000..6dcae495c --- /dev/null +++ b/ropetest-package-fixtures/external_fixturepkg/pyproject.toml @@ -0,0 +1,18 @@ +[build-system] +requires = ["flit-core >= 3.8"] +build-backend = "flit_core.buildapi" + +[project] +name = "external_fixturepkg" +version = "1.0.0" +description = "Just an example project built with build/flit for testing purpose" +readme = "README.md" +requires-python = ">= 3.7" + +dependencies = [ ] + +[project.scripts] +pyproject-build = "build.__main__:entrypoint" + +[tool.flit.sdist] +exclude = ["**/__pycache__", "**/*.egg-info"] diff --git a/ropetest-package-fixtures/external_fixturepkg/src/external_fixturepkg/__init__.py b/ropetest-package-fixtures/external_fixturepkg/src/external_fixturepkg/__init__.py new file mode 100644 index 000000000..8cf1e00f3 --- /dev/null +++ b/ropetest-package-fixtures/external_fixturepkg/src/external_fixturepkg/__init__.py @@ -0,0 +1,2 @@ +def sample_function(): + pass diff --git a/ropetest-package-fixtures/external_fixturepkg/src/external_fixturepkg/__main__.py b/ropetest-package-fixtures/external_fixturepkg/src/external_fixturepkg/__main__.py new file mode 100644 index 000000000..e6d098dee --- /dev/null +++ b/ropetest-package-fixtures/external_fixturepkg/src/external_fixturepkg/__main__.py @@ -0,0 +1,6 @@ +def main() -> None: + pass + + +if __name__ == '__main__': + main() diff --git a/ropetest-package-fixtures/external_fixturepkg/src/external_fixturepkg/mod1.py b/ropetest-package-fixtures/external_fixturepkg/src/external_fixturepkg/mod1.py new file mode 100644 index 000000000..3c74911b3 --- /dev/null +++ b/ropetest-package-fixtures/external_fixturepkg/src/external_fixturepkg/mod1.py @@ -0,0 +1 @@ +foo = None diff --git a/ropetest/conftest.py b/ropetest/conftest.py index ce8a1eab5..47515d31e 100644 --- a/ropetest/conftest.py +++ b/ropetest/conftest.py @@ -1,4 +1,6 @@ import pathlib +import sys +from subprocess import check_call import pytest @@ -13,13 +15,6 @@ def project(): testutils.remove_project(project) -@pytest.fixture -def project2(): - project = testutils.sample_project("another_project") - yield project - testutils.remove_project(project) - - @pytest.fixture def project_path(project): yield pathlib.Path(project.address) @@ -52,3 +47,17 @@ def pkg1(project) -> resources.Folder: @pytest.fixture def mod2(project, pkg1) -> resources.Folder: return testutils.create_module(project, "mod2", pkg1) + + +@pytest.fixture(scope="session") +def external_fixturepkg(): + check_call([ + sys.executable, + "-m", + "pip", + "install", + "--force-reinstall", + "ropetest-package-fixtures/external_fixturepkg/dist/external_fixturepkg-1.0.0-py3-none-any.whl", + ]) + yield + check_call([sys.executable, "-m", "pip", "uninstall", "--yes", "external-fixturepkg"]) diff --git a/ropetest/contrib/autoimport/conftest.py b/ropetest/contrib/autoimport/conftest.py index a32913046..29bcba4c0 100644 --- a/ropetest/contrib/autoimport/conftest.py +++ b/ropetest/contrib/autoimport/conftest.py @@ -24,19 +24,18 @@ def typing_path(): @pytest.fixture -def build_env_path(): - from build import env - - yield pathlib.Path(env.__file__) +def example_external_package_module_path(external_fixturepkg): + from external_fixturepkg import mod1 + yield pathlib.Path(mod1.__file__) @pytest.fixture -def build_path(): - import build +def example_external_package_path(external_fixturepkg): + import external_fixturepkg # Uses __init__.py so we need the parent - yield pathlib.Path(build.__file__).parent + yield pathlib.Path(external_fixturepkg.__file__).parent @pytest.fixture diff --git a/ropetest/contrib/autoimport/utilstest.py b/ropetest/contrib/autoimport/utilstest.py index acf2fb9e5..511e1a541 100644 --- a/ropetest/contrib/autoimport/utilstest.py +++ b/ropetest/contrib/autoimport/utilstest.py @@ -14,31 +14,35 @@ def test_get_package_source_not_project(mod1_path): assert utils.get_package_source(mod1_path, None, "") == Source.UNKNOWN -def test_get_package_source_pytest(build_path): +def test_get_package_source_pytest(example_external_package_path): # pytest is not installed as part of the standard library # but should be installed into site_packages, # so it should return Source.SITE_PACKAGE - assert utils.get_package_source(build_path, None, "build") == Source.SITE_PACKAGE + source = utils.get_package_source(example_external_package_path, None, "mod1") + assert source == Source.SITE_PACKAGE def test_get_package_source_typing(typing_path): - assert utils.get_package_source(typing_path, None, "typing") == Source.STANDARD def test_get_modname_project_no_add(mod1_path, project_path): - assert utils.get_modname_from_path(mod1_path, project_path, False) == "mod1" def test_get_modname_single_file(typing_path): - assert utils.get_modname_from_path(typing_path, typing_path) == "typing" -def test_get_modname_folder(build_path, build_env_path): - - assert utils.get_modname_from_path(build_env_path, build_path) == "build.env" +def test_get_modname_folder( + example_external_package_path, + example_external_package_module_path, +): + modname = utils.get_modname_from_path( + example_external_package_module_path, + example_external_package_path, + ) + assert modname == "external_fixturepkg.mod1" def test_get_package_tuple_sample(project_path): diff --git a/ropetest/contrib/autoimporttest.py b/ropetest/contrib/autoimporttest.py index 94bb033bb..d74135fff 100644 --- a/ropetest/contrib/autoimporttest.py +++ b/ropetest/contrib/autoimporttest.py @@ -1,6 +1,7 @@ import unittest from rope.contrib.autoimport import sqlite as autoimport +from rope.contrib.autoimport.defs import Alias from ropetest import testutils @@ -110,13 +111,6 @@ def test_handling_builtin_modules(self): self.importer.update_module("sys") self.assertIn("sys", self.importer.get_modules("exit")) - def test_search_submodule(self): - self.importer.update_module("build") - import_statement = ("from build import env", "env") - self.assertIn(import_statement, self.importer.search("env", exact_match=True)) - self.assertIn(import_statement, self.importer.search("en")) - self.assertIn(import_statement, self.importer.search("env")) - def test_search_module(self): self.importer.update_module("os") import_statement = ("import os", "os") @@ -124,6 +118,30 @@ def test_search_module(self): self.assertIn(import_statement, self.importer.search("os")) self.assertIn(import_statement, self.importer.search("o")) + def test_search_alias(self): + self.mod2.write("myvar = None\n") + self.importer.update_resource(self.mod2) + self.importer.add_aliases([ + ("noMatch", "does_not_exists_this"), + ("hasMatch", "pkg.mod2"), + ]) + + self.assertEqual([], self.importer.search("noMatch", exact_match=True)) + + import_statement = ("import pkg.mod2 as hasMatch", "hasMatch") + self.assertIn(import_statement, self.importer.search("hasMatch", exact_match=True)) + self.assertIn(import_statement, self.importer.search("hasM")) + self.assertIn(import_statement, self.importer.search("h")) + + def test_alias_updated_from_prefs(self): + self.mod2.write("myvar = None\n") + self.project.prefs.autoimport.aliases = [("mod2_alias", "pkg.mod2")] + self.importer.clear_cache() + self.importer.update_resource(self.mod2) + import_statement = ("import pkg.mod2 as mod2_alias", "mod2_alias") + self.assertIn(import_statement, self.importer.search("mod2_alias", exact_match=True)) + self.assertIn(import_statement, self.importer.search("mod2", exact_match=False)) + def test_search(self): self.importer.update_module("typing") import_statement = ("from typing import Dict", "Dict") @@ -146,9 +164,10 @@ def test_skipping_directories_not_accessible_because_of_permission_error(self): # The single thread test takes much longer than the multithread test but is easier to debug single_thread = False self.importer.generate_modules_cache(single_thread=single_thread) - + # Create a temporary directory and set permissions to 000 - import tempfile, sys + import sys + import tempfile with tempfile.TemporaryDirectory() as dir: import os os.chmod(dir, 0o000) @@ -158,6 +177,16 @@ def test_skipping_directories_not_accessible_because_of_permission_error(self): self.assertGreater(len(self.importer._dump_all()), 0) +def test_search_submodule(external_fixturepkg): + project = testutils.sample_project(extension_modules=["sys"]) + importer = autoimport.AutoImport(project, observe=False) + importer.update_module("external_fixturepkg") + import_statement = ("from external_fixturepkg import mod1", "mod1") + assert import_statement in importer.search("mod1", exact_match=True) + assert import_statement in importer.search("mo") + assert import_statement in importer.search("mod1") + + class AutoImportObservingTest(unittest.TestCase): def setUp(self): super().setUp() diff --git a/ropetest/refactor/extracttest.py b/ropetest/refactor/extracttest.py index e8c2fc206..4b34d58d3 100644 --- a/ropetest/refactor/extracttest.py +++ b/ropetest/refactor/extracttest.py @@ -1149,7 +1149,7 @@ def xxx_test_raising_exception_on_function_parens(self): end = code.rindex(")") + 1 with self.assertRaises(rope.base.exceptions.RefactoringError): self.do_extract_method(code, start, end, "new_func") - + def test_raising_exception_on_incomplete_block(self): code = dedent("""\ if True: @@ -1177,7 +1177,7 @@ def test_raising_exception_on_incomplete_block_3(self): code = dedent("""\ if True: a = 1 - + b = 2 """) start = code.index("if") diff --git a/ropetest/refactor/renametest.py b/ropetest/refactor/renametest.py index 43b7335e7..78bc38d82 100644 --- a/ropetest/refactor/renametest.py +++ b/ropetest/refactor/renametest.py @@ -1,6 +1,7 @@ import sys import unittest from textwrap import dedent +from rope.base import exceptions import rope.base.codeanalyze import rope.refactor.occurrences @@ -9,7 +10,7 @@ from ropetest import testutils -class RenameRefactoringTest(unittest.TestCase): +class RenameTestMixin: def setUp(self): super().setUp() self.project = testutils.sample_project() @@ -18,11 +19,11 @@ def tearDown(self): testutils.remove_project(self.project) super().tearDown() - def _local_rename(self, source_code, offset, new_name): + def _local_rename(self, source_code, offset, new_name, **kwds): testmod = testutils.create_module(self.project, "testmod") testmod.write(source_code) changes = Rename(self.project, testmod, offset).get_changes( - new_name, resources=[testmod] + new_name, resources=[testmod], **kwds ) self.project.do(changes) return testmod.read() @@ -31,6 +32,8 @@ def _rename(self, resource, offset, new_name, **kwds): changes = Rename(self.project, resource, offset).get_changes(new_name, **kwds) self.project.do(changes) + +class RenameRefactoringTest(RenameTestMixin, unittest.TestCase): def test_local_variable_but_not_parameter(self): code = dedent("""\ a = 10 @@ -748,90 +751,6 @@ def __init__(self): mod2.read(), ) - def test_renaming_methods_in_subclasses(self): - mod = testutils.create_module(self.project, "mod1") - mod.write(dedent("""\ - class A(object): - def a_method(self): - pass - class B(A): - def a_method(self): - pass - """)) - - self._rename( - mod, mod.read().rindex("a_method") + 1, "new_method", in_hierarchy=True - ) - self.assertEqual( - dedent("""\ - class A(object): - def new_method(self): - pass - class B(A): - def new_method(self): - pass - """), - mod.read(), - ) - - def test_renaming_methods_in_sibling_classes(self): - mod = testutils.create_module(self.project, "mod1") - mod.write(dedent("""\ - class A(object): - def a_method(self): - pass - class B(A): - def a_method(self): - pass - class C(A): - def a_method(self): - pass - """)) - - self._rename( - mod, mod.read().rindex("a_method") + 1, "new_method", in_hierarchy=True - ) - self.assertEqual( - dedent("""\ - class A(object): - def new_method(self): - pass - class B(A): - def new_method(self): - pass - class C(A): - def new_method(self): - pass - """), - mod.read(), - ) - - def test_not_renaming_methods_in_hierarchies(self): - mod = testutils.create_module(self.project, "mod1") - mod.write(dedent("""\ - class A(object): - def a_method(self): - pass - class B(A): - def a_method(self): - pass - """)) - - self._rename( - mod, mod.read().rindex("a_method") + 1, "new_method", in_hierarchy=False - ) - self.assertEqual( - dedent("""\ - class A(object): - def a_method(self): - pass - class B(A): - def new_method(self): - pass - """), - mod.read(), - ) - def test_undoing_refactorings(self): mod1 = testutils.create_module(self.project, "mod1") mod1.write(dedent("""\ @@ -1494,6 +1413,175 @@ def test_renaming_modules_aliased_many_dots(self): ) self.assertEqual("import new_json.utils.a as stdlib_json_utils\n", mod2.read()) + def test_rename_refuses_renaming_to_python_keyword(self): + with self.assertRaises(exceptions.RefactoringError, msg="Invalid refactoring target name. 'class' is a Python keyword."): + self._local_rename("a_var = 20\n", 2, "class") + + +class RenameRefactoringWithSuperclassTest(RenameTestMixin, unittest.TestCase): + ORIGINAL_CODE = dedent("""\ + class Parent: + def a_method(self): + pass + + class Child(Parent): + def a_method(self, strg): + return super(Child, self).a_method(strg, *args, **kwargs) + """) + BOTH_RENAMED = dedent("""\ + class Parent: + def new_method(self): + pass + + class Child(Parent): + def new_method(self, strg): + return super(Child, self).new_method(strg, *args, **kwargs) + """) + + PARENT_RENAMED = dedent("""\ + class Parent: + def new_method(self): + pass + + class Child(Parent): + def a_method(self, strg): + return super(Child, self).new_method(strg, *args, **kwargs) + """) + + CHILD_RENAMED = dedent("""\ + class Parent: + def a_method(self): + pass + + class Child(Parent): + def new_method(self, strg): + return super(Child, self).a_method(strg, *args, **kwargs) + """) + + FROM_PARENT = "a_method(self)" # from Parent.a_method + FROM_CHILD = "a_method(self, strg" # from Child.a_method + FROM_CALLER = "a_method(strg, *args" # from super() line + + def test_rename_with_superclass_in_hierarchy_from_parent(self): + code = self.ORIGINAL_CODE + offset = code.index(self.FROM_PARENT) + refactored = self._local_rename(code, offset, "new_method", in_hierarchy=True) + self.assertEqual(refactored, self.BOTH_RENAMED) + + def test_rename_with_superclass_not_in_hierarchy_from_parent(self): + code = self.ORIGINAL_CODE + offset = code.index(self.FROM_PARENT) + refactored = self._local_rename(code, offset, "new_method", in_hierarchy=False) + self.assertEqual(refactored, self.PARENT_RENAMED) + + def test_rename_with_superclass_in_hierarchy_from_child(self): + code = self.ORIGINAL_CODE + offset = code.index(self.FROM_CHILD) + refactored = self._local_rename(code, offset, "new_method", in_hierarchy=True) + self.assertEqual(refactored, self.BOTH_RENAMED) + + def test_rename_with_superclass_not_in_hierarchy_from_child(self): + code = self.ORIGINAL_CODE + offset = code.index(self.FROM_CHILD) + refactored = self._local_rename(code, offset, "new_method", in_hierarchy=False) + self.assertEqual(refactored, self.CHILD_RENAMED) + + def test_rename_with_superclass_in_hierarchy_from_caller(self): + code = self.ORIGINAL_CODE + offset = code.index(self.FROM_CALLER) + refactored = self._local_rename(code, offset, "new_method", in_hierarchy=True) + self.assertEqual(refactored, self.BOTH_RENAMED) + + def test_rename_with_superclass_not_in_hierarchy_from_caller(self): + code = self.ORIGINAL_CODE + offset = code.index(self.FROM_CALLER) + refactored = self._local_rename(code, offset, "new_method", in_hierarchy=False) + self.assertEqual(refactored, self.PARENT_RENAMED) + + def test_renaming_methods_in_subclasses(self): + mod = testutils.create_module(self.project, "mod1") + mod.write(dedent("""\ + class A(object): + def a_method(self): + pass + class B(A): + def a_method(self): + pass + """)) + + self._rename( + mod, mod.read().rindex("a_method") + 1, "new_method", in_hierarchy=True + ) + self.assertEqual( + dedent("""\ + class A(object): + def new_method(self): + pass + class B(A): + def new_method(self): + pass + """), + mod.read(), + ) + + def test_renaming_methods_in_sibling_classes(self): + mod = testutils.create_module(self.project, "mod1") + mod.write(dedent("""\ + class A(object): + def a_method(self): + pass + class B(A): + def a_method(self): + pass + class C(A): + def a_method(self): + pass + """)) + + self._rename( + mod, mod.read().rindex("a_method") + 1, "new_method", in_hierarchy=True + ) + self.assertEqual( + dedent("""\ + class A(object): + def new_method(self): + pass + class B(A): + def new_method(self): + pass + class C(A): + def new_method(self): + pass + """), + mod.read(), + ) + + def test_not_renaming_methods_in_hierarchies(self): + mod = testutils.create_module(self.project, "mod1") + mod.write(dedent("""\ + class A(object): + def a_method(self): + pass + class B(A): + def a_method(self): + pass + """)) + + self._rename( + mod, mod.read().rindex("a_method") + 1, "new_method", in_hierarchy=False + ) + self.assertEqual( + dedent("""\ + class A(object): + def a_method(self): + pass + class B(A): + def new_method(self): + pass + """), + mod.read(), + ) + class ChangeOccurrencesTest(unittest.TestCase): def setUp(self):