pax_global_header00006660000000000000000000000064145765366130014532gustar00rootroot0000000000000052 comment=ecf708396630736cdf280113bc1903055ab563b8 pylsp-rope-0.1.16/000077500000000000000000000000001457653661300137315ustar00rootroot00000000000000pylsp-rope-0.1.16/.github/000077500000000000000000000000001457653661300152715ustar00rootroot00000000000000pylsp-rope-0.1.16/.github/ISSUE_TEMPLATE.md000066400000000000000000000004571457653661300200040ustar00rootroot00000000000000* pylsp-rope version: * Text editor/IDE/LSP Client: * Python version: * Operating System: ### Description Describe what you were trying to get done. Tell us what happened, what went wrong, and what you expected to happen. ### Details ``` If there was a crash, please include the traceback here. ``` pylsp-rope-0.1.16/.github/workflows/000077500000000000000000000000001457653661300173265ustar00rootroot00000000000000pylsp-rope-0.1.16/.github/workflows/python-publish.yml000066400000000000000000000020771457653661300230440ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. name: Upload Python Package on: release: types: [published] jobs: deploy: runs-on: ubuntu-latest environment: publishing permissions: id-token: write steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.9' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: python -m build - name: Publish package if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 pylsp-rope-0.1.16/.github/workflows/run-test.yml000066400000000000000000000026131457653661300216340ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Tests on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install .[test] - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | pytest --cov - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} slug: python-rope/pylsp-rope pylsp-rope-0.1.16/.gitignore000066400000000000000000000023161457653661300157230ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json # IDE settings .vscode/ .idea/ # ctags tags pylsp-rope-0.1.16/AUTHORS.txt000066400000000000000000000002311457653661300156130ustar00rootroot00000000000000======= Credits ======= Development Lead ---------------- * Lie Ryan Contributors ------------ None yet. Why not be the first? pylsp-rope-0.1.16/CONTRIBUTING.md000066400000000000000000000033461457653661300161700ustar00rootroot00000000000000## Developing Install development dependencies with (you might want to create a virtualenv first): ``` bash git clone https://github.com/python-rope/pylsp-rope.git pylsp-rope cd pylsp-rope pip install -e '.[dev]' ``` ### Enabling logging Run pylsp in development mode, enable logs: ``` bash pylsp -v --log-file /tmp/pylsp.log ``` Vim users should refer to [Rope in Vim or Neovim](https://github.com/python-rope/rope/wiki/Rope-in-Vim-or-Neovim) for how to configure their LSP client to run `pylsp` in development mode. ### Enabling tcp mode Optionally, run in tcp mode if you want to be able to use the standard input/output for something else, for example when using IPython or pudb, run this from terminal: ``` bash pylsp -v --tcp --port 8772 --log-file /tmp/pylsp.log ``` #### Connecting to tcp mode pylsp from lsp-vim ``` vim autocmd User lsp_setup call lsp#register_server({ \ 'name': 'pylsp-debug', \ 'cmd': ["nc", "localhost", "8772"], \ 'allowlist': ['python'], \ }) ``` TODO: document how to connect to pylsp via pylsp from LSP clients. ### Testing Run `pytest` to run plugin tests. ## Publishing If this is your first time publishing to PyPI, follow the instruction at [Twine docs](https://packaging.python.org/guides/distributing-packages-using-setuptools/#create-an-account) to create an PyPI account and setup Twine. FIXME: update this to use the Github Publishing workflow 1. Update version number in `setup.cfg`. 2. Tag the release: ``` bash git tag --sign 0.1.3 git push origin main 0.1.3 ``` 3. Github Actions should publish to PyPI shortly. Verify the publishing are successful at https://pypi.org/project/pylsp-rope/#history. 4. Upload release assets to Github Release (fixme: find a way to automate this) pylsp-rope-0.1.16/LICENSE000066400000000000000000000020531457653661300147360ustar00rootroot00000000000000MIT License Copyright (c) 2021, Lie Ryan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pylsp-rope-0.1.16/MANIFEST.in000066400000000000000000000000341457653661300154640ustar00rootroot00000000000000recursive-include test *.py pylsp-rope-0.1.16/README.md000066400000000000000000000123131457653661300152100ustar00rootroot00000000000000# pylsp-rope [![Tests](https://github.com/python-rope/pylsp-rope/actions/workflows/run-test.yml/badge.svg)](https://github.com/python-rope/pylsp-rope/actions/workflows/run-test.yml) [![codecov](https://codecov.io/gh/python-rope/pylsp-rope/graph/badge.svg?token=LMO7PW0AEK)](https://codecov.io/gh/python-rope/pylsp-rope) Extended refactoring capabilities for Python LSP Server using [Rope](https://github.com/python-rope/rope). This is a plugin for [Python LSP Server](https://github.com/python-lsp/python-lsp-server), so you also need to have it installed. python-lsp-server already has basic built-in support for using Rope, but it's currently limited to just renaming and completion. Installing this plugin adds more refactoring functionality to python-lsp-server. ## Installation To use this plugin, you need to install this plugin in the same virtualenv as python-lsp-server itself. ``` bash pip install pylsp-rope ``` Then run `pylsp` as usual, the plugin will be auto-discovered by python-lsp-server if you've installed it to the right environment. On Vim, refer to [Rope in Vim or Neovim](https://github.com/python-rope/rope/wiki/Rope-in-Vim-or-Neovim). For other editors, refer to your IDE/text editor's documentation on how to setup a language server. ## Configuration You can enable rename support using pylsp-rope with workspace config key `pylsp.plugins.pylsp_rope.rename`. Note that this differs from the config key `pylsp.plugins.rope_rename.enabled` that is used for the rope rename implementation using the python-lsp-rope's builtin `rope_rename` plugin. To avoid confusion, avoid enabling more than one python-lsp-server rename plugin. ## Features This plugin adds the following features to python-lsp-server: Rename: - rename everything: classes, functions, modules, packages (disabled by default) Code Action: - extract method - extract variable - inline method/variable/parameter - use function - method to method object - convert local variable to field - organize imports - introduce parameter - generate variable/function/class from undefined variable Refer to [Rope documentation](https://github.com/python-rope/rope/blob/master/docs/overview.rst) for more details on how these refactoring works. ## Usage ### Rename When Rename is triggered, rename the symbol under the cursor. If the symbol under the cursor points to a module/package, it will move that module/package files. ### Extract method Variants: - Extract method - Extract global method - Extract method including similar statements - Extract global method including similar statements When CodeAction is triggered and the cursor is on any block of code, extract that expression into a method. Optionally, similar statements can also be extracted. ### Extract variable Variants: - Extract variable - Extract global variable - Extract variable including similar statements - Extract global variable including similar statements When CodeAction is triggered and the cursor is on a expression, extract that expression into a variable. Optionally, similar statements can also be extracted. ### Inline When CodeAction is triggered and the cursor is on a resolvable Python variable, replace all calls to that method with the method body. ### Use function When CodeAction is triggered and the cursor is on the function name of a `def` statement, try to replace code whose AST matches the selected function with a call to the function. ### Method to method object When CodeAction is triggered and the cursor is on the function name of a `def` statement, create a callable class to replace that method. You may want to inline the method afterwards to remove the indirection. ### Convert local variable to field When CodeAction is triggered wand the cursor is on a local variable inside a method, convert that local variable to an attribute. ### Organize import Trigger CodeAction anywhere in a Python file to organize imports. ### Introduce parameter When CodeAction is triggered and the cursor is selecting a Python variable or attribute, make that variable/attribute a parameter. ### Generate code Variants: - [x] Generate variable - [x] Generate function - [x] Generate class - [ ] Generate module - [ ] Generate package When CodeAction is triggered and the cursor is on an undefined Python variable, generate an empty variable/function/class/module/package for that name. ## Caveat Support for working on unsaved document is currently experimental. This plugin is in early development, so expect some bugs. Please report in [Github issue tracker](https://github.com/python-lsp/python-lsp-server/issues) if you had any issues with the plugin. ## Developing See [CONTRIBUTING.md](https://github.com/python-rope/pylsp-rope/blob/main/CONTRIBUTING.md). ## Packaging status [![Packaging status](https://repology.org/badge/vertical-allrepos/python:pylsp-rope.svg)](https://repology.org/project/python:pylsp-rope/versions) [![Packaging status](https://repology.org/badge/vertical-allrepos/python:lsp-rope.svg)](https://repology.org/project/python:lsp-rope/versions) ## Credits This package was created with [Cookiecutter](https://github.com/audreyr/cookiecutter) from [python-lsp/cookiecutter-pylsp-plugin](https://github.com/python-lsp/cookiecutter-pylsp-plugin) project template. pylsp-rope-0.1.16/pylsp_rope/000077500000000000000000000000001457653661300161255ustar00rootroot00000000000000pylsp-rope-0.1.16/pylsp_rope/__init__.py000066400000000000000000000000001457653661300202240ustar00rootroot00000000000000pylsp-rope-0.1.16/pylsp_rope/commands.py000066400000000000000000000011601457653661300202760ustar00rootroot00000000000000COMMAND_REFACTOR_EXTRACT_METHOD = "pylsp_rope.refactor.extract.method" COMMAND_REFACTOR_EXTRACT_VARIABLE = "pylsp_rope.refactor.extract.variable" COMMAND_REFACTOR_INLINE = "pylsp_rope.refactor.inline" COMMAND_REFACTOR_USE_FUNCTION = "pylsp_rope.refactor.use_function" COMMAND_REFACTOR_METHOD_TO_METHOD_OBJECT = "pylsp_rope.refactor.method_to_method_object" COMMAND_REFACTOR_LOCAL_TO_FIELD = "pylsp_rope.refactor.local_to_field" COMMAND_SOURCE_ORGANIZE_IMPORT = "pylsp_rope.source.organize_import" COMMAND_INTRODUCE_PARAMETER = "pylsp_rope.refactor.introduce_parameter" COMMAND_GENERATE_CODE = "pylsp_rope.quickfix.generate" pylsp-rope-0.1.16/pylsp_rope/lsp_diff.py000066400000000000000000000023741457653661300202730ustar00rootroot00000000000000import difflib from typing import Iterator, List, Tuple, cast from pylsp_rope.text import Position from pylsp_rope.typing import TextEdit, Line, LineNumber _DifflibOpcode = Tuple[str, LineNumber, LineNumber, LineNumber, LineNumber] def _difflib_ops_to_text_edit_ops( opcode: _DifflibOpcode, lines: List[Line] ) -> TextEdit: op, start_old, end_old, start_new, end_new = opcode if op == "replace" or op == "insert": new_text = "".join(lines[start_new:end_new]) elif op == "delete": new_text = "" else: assert False, opcode return { "range": {"start": Position(start_old), "end": Position(end_old)}, "newText": new_text, } def lsp_diff(lines_old: List[Line], lines_new: List[Line]) -> Iterator[TextEdit]: """ Given two sequences of lines, produce a [TextEdit][1] changeset. [1]: https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textEdit """ matcher = difflib.SequenceMatcher(a=lines_old, b=lines_new) for opcode in matcher.get_opcodes(): if opcode[0] == "equal": continue text_edit_ops = _difflib_ops_to_text_edit_ops( cast(_DifflibOpcode, opcode), lines_new ) yield text_edit_ops pylsp-rope-0.1.16/pylsp_rope/plugin.py000066400000000000000000000146441457653661300200060ustar00rootroot00000000000000import logging from typing import List, Optional from pylsp import hookimpl, uris from rope.base import libutils from pylsp.lsp import MessageType from rope.refactor.rename import Rename from pylsp_rope import refactoring, typing, commands from pylsp_rope.project import ( get_project, get_resource, get_resources, rope_changeset_to_workspace_edit, new_project, ) logger = logging.getLogger(__name__) @hookimpl def pylsp_settings(): logger.info("Initializing pylsp_rope") return { "plugins": { # "autopep8_format": {"enabled": False}, # "definition": {"enabled": False}, # "flake8_lint": {"enabled": False}, # "folding": {"enabled": False}, # "highlight": {"enabled": False}, # "hover": {"enabled": False}, # "jedi_completion": {"enabled": False}, # "jedi_rename": {"enabled": False}, # "mccabe_lint": {"enabled": False}, # "preload_imports": {"enabled": False}, # "pycodestyle_lint": {"enabled": False}, # "pydocstyle_lint": {"enabled": False}, # "pyflakes_lint": {"enabled": False}, # "pylint_lint": {"enabled": False}, # "references": {"enabled": False}, # "rope_completion": {"enabled": False}, # "rope_rename": {"enabled": False}, "pylsp_rope": { "enabled": True, "rename": False, }, # "signature": {"enabled": False}, # "symbols": {"enabled": False}, # "yapf_format": {"enabled": False}, }, } @hookimpl def pylsp_commands(config, workspace) -> List[str]: return [getattr(commands, cmd) for cmd in dir(commands) if not cmd.startswith("_")] @hookimpl def pylsp_code_actions( config, workspace, document, range, context, ) -> List[typing.CodeAction]: logger.info("textDocument/codeAction: %s %s %s", document, range, context) class info: current_document, resource = get_resource(workspace, document.uri) position = range["start"] start_offset = current_document.offset_at_position(range["start"]) end_offset = current_document.offset_at_position(range["end"]) selected_text = document.source[start_offset:end_offset] project = get_project(workspace) for resource in get_resources(workspace, workspace.documents.keys()): project.pycore._invalidate_resource_cache(resource) commands = {} commands.update( refactoring.CommandRefactorExtractMethod.get_code_actions( workspace, document=document, range=range, ), ) commands.update( refactoring.CommandRefactorExtractVariable.get_code_actions( workspace, document=document, range=range, ), ) commands.update( { "Inline method/variable/parameter": refactoring.CommandRefactorInline( workspace, document_uri=document.uri, position=info.position, ), "Use function": refactoring.CommandRefactorUseFunction( workspace, document_uri=document.uri, position=info.position, ), "Use function for current file only": refactoring.CommandRefactorUseFunction( workspace, document_uri=document.uri, position=info.position, documents=[document.uri], ), "To method object": refactoring.CommandRefactorMethodToMethodObject( workspace, document_uri=document.uri, position=info.position, ), "Convert local variable to field": refactoring.CommandRefactorLocalToField( workspace, document_uri=document.uri, position=info.position, ), "Organize import": refactoring.CommandSourceOrganizeImport( workspace, document_uri=document.uri, ), "Introduce parameter": refactoring.CommandIntroduceParameter( workspace, document_uri=document.uri, position=info.position, ), } ) commands.update( refactoring.GenerateCode.get_code_actions( workspace, document=document, position=info.position, ), ) return [ cmd.get_code_action(title=title) for title, cmd in commands.items() if cmd.is_valid(info) ] @hookimpl def pylsp_execute_command(config, workspace, command, arguments): logger.info("workspace/executeCommand: %s %s", command, arguments) commands = {cmd.name: cmd for cmd in refactoring.Command.__subclasses__()} try: return commands[command](workspace, **arguments[0])( # FIXME: Hardcode executeCommand to use WorkspaceEditWithChanges # We need to upgrade this at some point. workspace_edit_format=["changes"], ) except Exception as exc: logger.exception( "Exception when doing workspace/executeCommand: %s", str(exc), exc_info=exc, ) workspace.show_message( f"pylsp-rope: {exc}", msg_type=MessageType.Error, ) @hookimpl def pylsp_rename( config, workspace, document, position, new_name, ) -> Optional[typing.WorkspaceEdit]: cfg = config.plugin_settings("pylsp_rope", document_path=document.uri) if not cfg.get("rename", False): return None logger.info("textDocument/rename: %s %s %s", document, position, new_name) project = new_project(workspace) # FIXME: we shouldn't have to always keep creating new projects here document, resource = get_resource(workspace, document.uri, project=project) rename = Rename( project=project, resource=resource, offset=document.offset_at_position(position), ) logger.debug( "Executing rename of %s to %s", document.word_at_position(position), new_name, ) rope_changeset = rename.get_changes(new_name, in_hierarchy=True, docs=True) logger.debug("Finished rename: %s", rope_changeset.changes) workspace_edit = rope_changeset_to_workspace_edit( workspace, rope_changeset, ) return workspace_edit pylsp-rope-0.1.16/pylsp_rope/project.py000066400000000000000000000134321457653661300201500ustar00rootroot00000000000000from __future__ import annotations import logging from functools import lru_cache from typing import List, Dict, Tuple, Optional, Literal, cast from pylsp import uris, workspace from rope.base import libutils from rope.base.fscommands import FileSystemCommands from pylsp_rope import rope from pylsp_rope.lsp_diff import lsp_diff from pylsp_rope.typing import ( WorkspaceEditWithChanges, WorkspaceEditWithDocumentChanges, WorkspaceEdit, DocumentUri, TextEdit, Line, TextDocumentEdit, ) logger = logging.getLogger(__name__) @lru_cache(maxsize=None) def get_project(workspace) -> rope.Project: """Get a cached rope Project or create one if it doesn't exist yet""" return new_project(workspace) def new_project(workspace) -> rope.Project: """ Always create a new Project, some operations like rename seems to have problems when using the cached Project """ fscommands = WorkspaceFileCommands(workspace) return rope.Project(workspace.root_path, fscommands=fscommands) def get_resource( workspace, document_uri: DocumentUri, *, project: rope.Project = None, ) -> Tuple[workspace.Document, rope.Resource]: """ Return a Document and Resource related to an LSP Document. `project` must be provided if not using instances of rope Project from `pylsp_rope.project.get_project()`. """ document = workspace.get_document(document_uri) project = project or get_project(workspace) resource = libutils.path_to_resource(project, document.path) return document, resource def get_resources(workspace, documents: List[DocumentUri]) -> List[rope.Resource]: if documents is None: return None return [get_resource(workspace, document_uri)[1] for document_uri in documents] def get_document(workspace, resource: rope.Resource) -> workspace.Document: return workspace.get_document(uris.from_fs_path(resource.real_path)) def _get_contents(change: rope.Change) -> Tuple[List[Line], List[Line]]: old = change.old_contents new = change.new_contents if old is None: if change.resource.exists(): old = change.resource.read() else: old = "" return old.splitlines(keepends=True), new.splitlines(keepends=True) def convert_workspace_edit_document_changes_to_changes( workspace_edit: WorkspaceEditWithDocumentChanges, ) -> WorkspaceEditWithChanges: workspace_changeset: Dict[DocumentUri, List[TextEdit]] = {} for change in workspace_edit["documentChanges"] or []: document_changes = workspace_changeset.setdefault( change["textDocument"]["uri"], [], ) document_changes.extend(change["edits"]) return { "changes": workspace_changeset, } def _rope_changeset_to_workspace_edit( workspace, rope_changeset: rope.ChangeSet ) -> WorkspaceEditWithDocumentChanges: workspace_changeset: List[TextDocumentEdit] = [] for change in rope_changeset.changes: lines_old, lines_new = _get_contents(change) document = get_document(workspace, change.resource) workspace_changeset.append( { "textDocument": { "uri": document.uri, "version": document.version, }, "edits": list(lsp_diff(lines_old, lines_new)), } ) return { "documentChanges": workspace_changeset, } WorkspaceEditFormat = Literal["changes", "documentChanges"] DEFAULT_WORKSPACE_EDIT_FORMAT: List[WorkspaceEditFormat] = ["changes"] def rope_changeset_to_workspace_edit( workspace, rope_changeset: rope.ChangeSet, workspace_edit_format: List[WorkspaceEditFormat] = DEFAULT_WORKSPACE_EDIT_FORMAT, ) -> WorkspaceEdit: assert len(workspace_edit_format) > 0 documentChanges: WorkspaceEditWithDocumentChanges = ( _rope_changeset_to_workspace_edit( workspace, rope_changeset, ) ) workspace_edit: dict = {} if "changes" in workspace_edit_format: changes: WorkspaceEditWithChanges = ( convert_workspace_edit_document_changes_to_changes(documentChanges) ) workspace_edit.update(changes) if "documentChanges" in workspace_edit_format: workspace_edit.update(documentChanges) return cast(WorkspaceEdit, workspace_edit) def apply_rope_changeset( workspace, rope_changeset: rope.ChangeSet, workspace_edit_format: List[WorkspaceEditFormat] = DEFAULT_WORKSPACE_EDIT_FORMAT, ) -> None: workspace_edit = rope_changeset_to_workspace_edit( workspace, rope_changeset, workspace_edit_format=workspace_edit_format, ) logger.info("applying workspace edit: %s", workspace_edit) workspace.apply_edit(workspace_edit) class WorkspaceFileCommands(object): def __init__(self, workspace): self.workspace = workspace self.normal_actions = FileSystemCommands() def create_file(self, path): return self.normal_actions.create_file(path) def create_folder(self, path): return self.normal_actions.create_folder(path) def move(self, path, new_location): return self.normal_actions.move(path, new_location) def remove(self, path): return self.normal_actions.remove(path) def write(self, path, data): return self.normal_actions.write(path, data) def read(self, path): document_uri = uris.from_fs_path(path) document = self.workspace.get_maybe_document(document_uri) if document is None: content = self.normal_actions.read(path) logger.info('reading from filesystem: "%s":', path) return content else: content = document.source.encode("utf-8") logger.info('reading from workspace: "%s":', path) return content pylsp-rope-0.1.16/pylsp_rope/refactoring.py000066400000000000000000000302521457653661300210040ustar00rootroot00000000000000import ast from typing import List, Optional from rope.contrib import generate from rope.refactor import ( extract, inline, method_object, usefunction, localtofield, importutils, introduce_parameter, ) from pylsp_rope import typing, commands from pylsp_rope.project import ( WorkspaceEditFormat, get_project, get_resource, get_resources, apply_rope_changeset, DEFAULT_WORKSPACE_EDIT_FORMAT, ) from pylsp_rope.typing import DocumentUri, CodeActionKind class Command: name: str title: str kind: CodeActionKind def __init__(self, workspace, **arguments): self.workspace = workspace self.arguments = arguments self.__dict__.update(**arguments) def __call__( self, *, workspace_edit_format: List[ WorkspaceEditFormat ] = DEFAULT_WORKSPACE_EDIT_FORMAT, ): rope_changeset = self.get_changes() if rope_changeset is not None: apply_rope_changeset( self.workspace, rope_changeset, workspace_edit_format, ) def get_changes(self): """ Calculate the rope changeset to perform this refactoring. """ def validate(self, info) -> None: """ Override this method to raise an exception if this refactoring command cannot be performed """ def is_valid(self, info): try: self.validate(info) except Exception: return False else: return True return False def get_code_action(self, title: str) -> typing.CodeAction: return { "title": title, "kind": self.kind, "command": { "title": title, "command": self.name, "arguments": [self.arguments], }, } @property # FIXME: backport cached_property def project(self): if not hasattr(self, "_project"): self._project = get_project(self.workspace) return self._project class CommandRefactorExtractMethod(Command): name = commands.COMMAND_REFACTOR_EXTRACT_METHOD kind: CodeActionKind = "refactor.extract" document_uri: DocumentUri range: typing.Range similar: bool global_: bool # FIXME: requires rope.refactor.extract._ExceptionalConditionChecker for proper checking # def _is_valid(self, info): # ... def get_changes(self): current_document, resource = get_resource(self.workspace, self.document_uri) refactoring = extract.ExtractMethod( project=self.project, resource=resource, start_offset=current_document.offset_at_position(self.range["start"]), end_offset=current_document.offset_at_position(self.range["end"]), ) rope_changeset = refactoring.get_changes( extracted_name="extracted_method", similar=self.similar, global_=self.global_, ) return rope_changeset @classmethod def get_code_actions(cls, workspace, document, range): return { "Extract method including similar statements": cls( workspace, document_uri=document.uri, range=range, global_=False, similar=True, ), "Extract method": cls( workspace, document_uri=document.uri, range=range, global_=False, similar=False, ), "Extract global method including similar statements": cls( workspace, document_uri=document.uri, range=range, global_=True, similar=True, ), "Extract global method": cls( workspace, document_uri=document.uri, range=range, global_=True, similar=False, ), } class CommandRefactorExtractVariable(Command): name = commands.COMMAND_REFACTOR_EXTRACT_VARIABLE kind: CodeActionKind = "refactor.extract" document_uri: DocumentUri range: typing.Range similar: bool global_: bool def validate(self, info): # FIXME: requires rope.refactor.extract._ExceptionalConditionChecker for proper checking ast.parse(info.selected_text, mode="eval") def get_changes(self): current_document, resource = get_resource(self.workspace, self.document_uri) refactoring = extract.ExtractVariable( project=self.project, resource=resource, start_offset=current_document.offset_at_position(self.range["start"]), end_offset=current_document.offset_at_position(self.range["end"]), ) rope_changeset = refactoring.get_changes( extracted_name="extracted_variable", similar=self.similar, global_=self.global_, ) return rope_changeset @classmethod def get_code_actions(cls, workspace, document, range): return { "Extract variable including similar statements": cls( workspace, document_uri=document.uri, range=range, global_=False, similar=True, ), "Extract variable": cls( workspace, document_uri=document.uri, range=range, global_=False, similar=False, ), "Extract global variable including similar statements": cls( workspace, document_uri=document.uri, range=range, global_=True, similar=True, ), "Extract global variable": cls( workspace, document_uri=document.uri, range=range, global_=True, similar=False, ), } class CommandRefactorInline(Command): name = commands.COMMAND_REFACTOR_INLINE kind: CodeActionKind = "refactor.inline" document_uri: DocumentUri position: typing.Range def validate(self, info): inline.create_inline( project=self.project, resource=info.resource, offset=info.current_document.offset_at_position(info.position), ) def get_changes(self): current_document, resource = get_resource(self.workspace, self.document_uri) refactoring = inline.create_inline( project=self.project, resource=resource, offset=current_document.offset_at_position(self.position), ) rope_changeset = refactoring.get_changes() return rope_changeset class CommandRefactorUseFunction(Command): name = commands.COMMAND_REFACTOR_USE_FUNCTION kind: CodeActionKind = "refactor" document_uri: DocumentUri documents: Optional[List[DocumentUri]] = None position: typing.Range def validate(self, info): usefunction.UseFunction( project=self.project, resource=info.resource, offset=info.current_document.offset_at_position(info.position), ) def get_changes(self): current_document, resource = get_resource(self.workspace, self.document_uri) refactoring = usefunction.UseFunction( project=self.project, resource=resource, offset=current_document.offset_at_position(self.position), ) resources = ( get_resources(self.workspace, self.documents) if self.documents is not None else None ) rope_changeset = refactoring.get_changes( resources=resources, ) return rope_changeset class CommandRefactorMethodToMethodObject(Command): name = commands.COMMAND_REFACTOR_METHOD_TO_METHOD_OBJECT kind: CodeActionKind = "refactor.rewrite" document_uri: DocumentUri position: typing.Range def validate(self, info): method_object.MethodObject( project=self.project, resource=info.resource, offset=info.current_document.offset_at_position(self.position), ) def get_changes(self): current_document, resource = get_resource(self.workspace, self.document_uri) refactoring = method_object.MethodObject( project=self.project, resource=resource, offset=current_document.offset_at_position(self.position), ) rope_changeset = refactoring.get_changes(classname="NewMethodObject") return rope_changeset class CommandRefactorLocalToField(Command): name = commands.COMMAND_REFACTOR_LOCAL_TO_FIELD kind: CodeActionKind = "refactor.rewrite" document_uri: DocumentUri position: typing.Range def validate(self, info): localtofield.LocalToField( project=self.project, resource=info.resource, offset=info.current_document.offset_at_position(self.position), ) def get_changes(self): current_document, resource = get_resource(self.workspace, self.document_uri) refactoring = localtofield.LocalToField( project=self.project, resource=resource, offset=current_document.offset_at_position(self.position), ) rope_changeset = refactoring.get_changes() return rope_changeset class CommandSourceOrganizeImport(Command): name = commands.COMMAND_SOURCE_ORGANIZE_IMPORT kind: CodeActionKind = "source.organizeImports" document_uri: DocumentUri def get_changes(self): current_document, resource = get_resource(self.workspace, self.document_uri) organizer = importutils.ImportOrganizer( project=self.project, ) rope_changeset = organizer.organize_imports( resource=resource, ) return rope_changeset class CommandIntroduceParameter(Command): name = commands.COMMAND_INTRODUCE_PARAMETER kind: CodeActionKind = "refactor" document_uri: DocumentUri position: typing.Range def validate(self, info): introduce_parameter.IntroduceParameter( project=self.project, resource=info.resource, offset=info.current_document.offset_at_position(self.position), ) def get_changes(self): current_document, resource = get_resource(self.workspace, self.document_uri) refactoring = introduce_parameter.IntroduceParameter( project=self.project, resource=resource, offset=current_document.offset_at_position(self.position), ) rope_changeset = refactoring.get_changes( new_parameter="new_parameter", ) return rope_changeset class GenerateCode(Command): """ Given an undefined symbol under cursor, generate an empty variable/function/class/module/package """ name = commands.COMMAND_GENERATE_CODE kind: CodeActionKind = "quickfix" document_uri: DocumentUri position: typing.Range generate_kind: str def validate(self, info): generate.create_generate( kind=self.generate_kind, project=self.project, resource=info.resource, offset=info.current_document.offset_at_position(self.position), ) def get_changes(self): current_document, resource = get_resource(self.workspace, self.document_uri) refactoring = generate.create_generate( kind=self.generate_kind, project=self.project, resource=resource, offset=current_document.offset_at_position(self.position), ) rope_changeset = refactoring.get_changes() return rope_changeset @classmethod def get_code_actions(cls, workspace, document, position): return { f"Generate {generate_kind}": cls( workspace, document_uri=document.uri, position=position, generate_kind=generate_kind, ) for generate_kind in ["variable", "function", "class", "module", "package"] } pylsp-rope-0.1.16/pylsp_rope/rope.py000066400000000000000000000003141457653661300174420ustar00rootroot00000000000000from rope.base.change import ChangeSet, Change from rope.base.project import Project from rope.base.resources import Resource __all__ = [ "Change", "ChangeSet", "Project", "Resource", ] pylsp-rope-0.1.16/pylsp_rope/text.py000066400000000000000000000063341457653661300174710ustar00rootroot00000000000000from typing import Tuple, Union, overload, Optional from pylsp_rope import typing from pylsp_rope.typing import LineNumber, CharNumber, Literal START_OF_LINE: Literal["^"] = "^" END_OF_LINE: Literal["$"] = "$" AutoLineNumber = Union[LineNumber, int] AutoCharNumber = Union[CharNumber, int] _CharNumberOrMarker = Union[AutoCharNumber, Literal["^", "$"]] _PrimitiveLineCharNumber = Union[ AutoLineNumber, Tuple[AutoLineNumber, Optional[_CharNumberOrMarker]] ] @overload def Position( line: Tuple[AutoLineNumber, Optional[_CharNumberOrMarker]], *, _default_character: _CharNumberOrMarker = CharNumber(0), ) -> typing.Position: ... @overload def Position( line: AutoLineNumber, *, _default_character: _CharNumberOrMarker = CharNumber(0), ) -> typing.Position: ... @overload def Position( line: AutoLineNumber, character: AutoCharNumber, ) -> typing.Position: ... @overload def Position( line: AutoLineNumber, character: Literal["^", "$"], ) -> typing.Position: ... def Position( line: _PrimitiveLineCharNumber, character: Optional[_CharNumberOrMarker] = None, *, _default_character: _CharNumberOrMarker = CharNumber(0), ) -> typing.Position: """ Returns a [Position](https://microsoft.github.io/language-server-protocol/specification#position) object for a document. `pos` can be: - Tuple[LineNumber, CharNumber] are passed directly to the object - int selects the start of the line - "^" the first non-blank character of the line - "$" the end of the line, which is the start of the next line Selects the start of line 4 >>> a = Position(4) >>> b = Position(4, 0) >>> c = Position((4, 0)) >>> assert a == b == c Selects the end of line 4: >>> c = Position(4, "$") >>> d = Position(5, 0) >>> assert c == d """ if isinstance(line, tuple): # assert ( # character is None # ), "If `line` is a tuple, then `character` must not be supplied" lineno, character = line else: lineno = line if character is None: character = _default_character if character == "$": lineno = LineNumber(lineno + 1) character = CharNumber(0) assert character != "^", "not implemented yet" return { "line": lineno, "character": character, } def Range( start: _PrimitiveLineCharNumber, end: Optional[_PrimitiveLineCharNumber] = None, ) -> typing.Range: """ Returns a [Range](https://microsoft.github.io/language-server-protocol/specification#range) object for a document. `start` and `end` accepts the same arguments as Position object. If `start` or `end` is an int, then the whole line is selected. Selects the whole line 4, including the line ending >>> a = Range(4) >>> b = Range(4, 4) >>> c = Range((4, 0), (5, 0)) >>> assert a == b == c Selects line 4-6 >>> d = Range(4, 6) >>> e = Range((4, 0), (7, 0)) >>> assert d == e """ if end is None: end = start return { "start": Position(start, _default_character=CharNumber(0)), "end": Position(end, _default_character=END_OF_LINE), } pylsp-rope-0.1.16/pylsp_rope/typing.py000066400000000000000000000054501457653661300200150ustar00rootroot00000000000000import sys from typing import List, Dict, Optional, NewType, Any, Union try: from typing import TypeGuard except ImportError: from typing_extensions import TypeGuard if sys.version_info >= (3, 8): from typing import TypedDict, Literal else: from typing_extensions import TypedDict, Literal ########################## ### Standard LSP types ### ########################## DocumentUri = NewType("DocumentUri", str) class Position(TypedDict): line: int character: int class Range(TypedDict): start: Position end: Position class TextDocumentIdentifier(TypedDict): uri: DocumentUri class OptionalVersionedTextDocumentIdentifier(TextDocumentIdentifier): version: Optional[int] class TextEdit(TypedDict): range: Range newText: str class TextDocumentEdit(TypedDict): textDocument: OptionalVersionedTextDocumentIdentifier edits: List[TextEdit] # FIXME: should be: list[TextEdit| AnnotatedTextEdit] class WorkspaceEditWithChanges(TypedDict): changes: Dict[DocumentUri, List[TextEdit]] # documentChanges: Optional[list[TextDocumentEdit]] # FIXME: should be: (TextDocumentEdit | CreateFile | RenameFile | DeleteFile)[] # changeAnnotations: ... class WorkspaceEditWithDocumentChanges(TypedDict): # changes: Optional[Dict[DocumentUri, List[TextEdit]]] documentChanges: List[ TextDocumentEdit ] # FIXME: should be: (TextDocumentEdit | CreateFile | RenameFile | DeleteFile)[] # changeAnnotations: ... WorkspaceEdit = Union[WorkspaceEditWithChanges, WorkspaceEditWithDocumentChanges] def is_workspace_edit_with_changes( workspace_edit: WorkspaceEdit, ) -> TypeGuard[WorkspaceEditWithChanges]: return "changes" in workspace_edit def is_workspace_edit_with_document_changes( workspace_edit: WorkspaceEdit, ) -> TypeGuard[WorkspaceEditWithDocumentChanges]: return "documentChanges" in workspace_edit class ApplyWorkspaceEditParams(TypedDict): label: Optional[str] edit: WorkspaceEdit class Command(TypedDict): title: str command: str arguments: Optional[List[Any]] CodeActionKind = Literal[ "", "quickfix", "refactor", "refactor.extract", "refactor.inline", "refactor.rewrite", "source", "source.organizeImports", "source.fixAll", ] class CodeAction(TypedDict): title: str kind: Optional[CodeActionKind] # diagnostics: Optional[List[Diagnostic]] # isPreferred: Optional[bool] # disabled: Optional[_CodeActionDisabledReason] # edit: Optional[WorkspaceEdit] command: Optional[Command] # data: Optional[Any] ######################## ### pylsp-rope types ### ######################## DocumentContent = NewType("DocumentContent", str) Line = NewType("Line", str) LineNumber = NewType("LineNumber", int) CharNumber = NewType("CharNumber", int) pylsp-rope-0.1.16/pyproject.toml000066400000000000000000000003031457653661300166410ustar00rootroot00000000000000[tool.black] exclude = '.ropeproject|test/fixtures' [tool.mypy] python_version = "3.8" warn_return_any = true warn_unused_configs = true ignore_missing_imports = true check_untyped_defs = true pylsp-rope-0.1.16/setup.cfg000066400000000000000000000021401457653661300155470ustar00rootroot00000000000000[metadata] name = pylsp-rope version = 0.1.16 author = Lie Ryan author_email = lie.1296@gmail.com url = https://github.com/python-rope/pylsp-rope description = Extended refactoring capabilities for Python LSP Server using Rope. long_description = file: README.md long_description_content_type = text/markdown license = MIT license classifiers = Programming Language :: Python Operating System :: OS Independent Development Status :: 2 - Pre-Alpha Intended Audience :: Developers Topic :: Text Editors :: Integrated Development Environments (IDE) Topic :: Software Development License :: OSI Approved :: MIT License [options] packages = find: install_requires = python-lsp-server rope>=0.21.0 typing-extensions; python_version < '3.10' python_requires = >= 3.6 [options.packages.find] exclude = test* [options.entry_points] pylsp = pylsp_rope = pylsp_rope.plugin [options.extras_require] # extras local dev environment dev = build pytest twine # extras for CI test runner test = flake8 pytest pytest-cov [pycodestyle] max-line-length = 88 pylsp-rope-0.1.16/setup.py000066400000000000000000000001051457653661300154370ustar00rootroot00000000000000from setuptools import setup if __name__ == "__main__": setup() pylsp-rope-0.1.16/test/000077500000000000000000000000001457653661300147105ustar00rootroot00000000000000pylsp-rope-0.1.16/test/__init__.py000066400000000000000000000000001457653661300170070ustar00rootroot00000000000000pylsp-rope-0.1.16/test/conftest.py000066400000000000000000000035361457653661300171160ustar00rootroot00000000000000from pathlib import Path from unittest.mock import Mock try: from importlib.resources import files as resources_files except ImportError: resources_files = None import pkg_resources import pytest from pylsp import uris from pylsp.config.config import Config from pylsp.workspace import Workspace, Document @pytest.fixture def config(workspace): """Return a config object.""" cfg = Config(workspace.root_uri, {}, 0, {}) cfg._plugin_settings = { "plugins": { "pylint": { "enabled": False, "args": [], "executable": None, }, }, } return cfg @pytest.fixture def workspace(tmpdir): """Return a workspace.""" ws = Workspace(uris.from_fs_path(str(tmpdir)), Mock()) ws._config = Config(ws.root_uri, {}, 0, {}) return ws @pytest.fixture def document(workspace): return create_document(workspace, "simple.py") @pytest.fixture def code_action_context(): # https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#codeActionKind code_action_kind = [ "", "quickfix", "refactor", "refactor.extract", "refactor.inline", "refactor.rewrite", "source", "source.organizeImports", "source.fixAll", ] return { "diagnostics": [], "only": code_action_kind, } def read_fixture_file(name): if resources_files: return (resources_files("test") / f"fixtures/{name}").read_text() else: return pkg_resources.resource_string(__name__, "fixtures/" + name).decode() def create_document(workspace, name): dest_path = Path(workspace.root_path) / name dest_path.write_text(read_fixture_file(name)) document_uri = uris.from_fs_path(str(dest_path)) return Document(document_uri, workspace) pylsp-rope-0.1.16/test/fixtures/000077500000000000000000000000001457653661300165615ustar00rootroot00000000000000pylsp-rope-0.1.16/test/fixtures/function.py000066400000000000000000000001421457653661300207550ustar00rootroot00000000000000def add(a, b): return a + b def main(): a, b = 10, 20 print(f"{a} + {b} = {a + b}") pylsp-rope-0.1.16/test/fixtures/generate_class.py000066400000000000000000000001121457653661300221040ustar00rootroot00000000000000def foo(): class undef_var(object): pass print(undef_var) pylsp-rope-0.1.16/test/fixtures/generate_function.py000066400000000000000000000001021457653661300226230ustar00rootroot00000000000000def foo(): def undef_var(): pass print(undef_var) pylsp-rope-0.1.16/test/fixtures/generate_variable.py000066400000000000000000000000651457653661300225730ustar00rootroot00000000000000def foo(): undef_var = None print(undef_var) pylsp-rope-0.1.16/test/fixtures/introduce_parameter.py000066400000000000000000000002311457653661300231630ustar00rootroot00000000000000import sys def main(new_parameter=sys.stdin): a = int(new_parameter.read()) b = 20 print(a + b) c = a + b a, b = 30, 40 print(a + b) pylsp-rope-0.1.16/test/fixtures/many_changes.py000066400000000000000000000002511457653661300215650ustar00rootroot00000000000000import os hello = "world" print(hello) roses = "here" are = "here" red = "here" violets = "here" are = "here" blue = "here" if True: os.path.join(hello, roses) pylsp-rope-0.1.16/test/fixtures/many_changes_inlined.py000066400000000000000000000002351457653661300232710ustar00rootroot00000000000000import os print("world") roses = "here" are = "here" red = "here" violets = "here" are = "here" blue = "here" if True: os.path.join("world", roses) pylsp-rope-0.1.16/test/fixtures/method.py000066400000000000000000000002451457653661300204140ustar00rootroot00000000000000import sys class MyClass: def my_method(self): local_var = 10 print(sys.stdin.read()) print(local_var) print(sys.stdin.read()) pylsp-rope-0.1.16/test/fixtures/method_local_to_field.py000066400000000000000000000002171457653661300234320ustar00rootroot00000000000000import sys class MyClass: def my_method(self): self.local_var = 10 print(sys.stdin.read()) print(self.local_var) pylsp-rope-0.1.16/test/fixtures/method_object.py000066400000000000000000000004221457653661300217370ustar00rootroot00000000000000def add(a, b): return NewMethodObject(a, b)() class NewMethodObject(object): def __init__(self, a, b): self.a = a self.b = b def __call__(self): return self.a + self.b def main(): a, b = 10, 20 print(f"{a} + {b} = {a + b}") pylsp-rope-0.1.16/test/fixtures/method_object_use_function.py000066400000000000000000000004751457653661300245300ustar00rootroot00000000000000import function def add(a, b): return NewMethodObject(a, b)() class NewMethodObject(object): def __init__(self, a, b): self.a = a self.b = b def __call__(self): return function.add(self.a, self.b) def main(): a, b = 10, 20 print(f"{a} + {b} = {function.add(a, b)}") pylsp-rope-0.1.16/test/fixtures/method_with_global_function.py000066400000000000000000000003341457653661300246730ustar00rootroot00000000000000import sys class MyClass: def my_method(self): local_var = 10 print(extracted_method()) print(local_var) print(sys.stdin.read()) def extracted_method(): return sys.stdin.read() pylsp-rope-0.1.16/test/fixtures/method_with_global_variable.py000066400000000000000000000003151457653661300246320ustar00rootroot00000000000000import sys class MyClass: def my_method(self): local_var = 10 print(extracted_variable) print(local_var) print(sys.stdin.read()) extracted_variable = sys.stdin.read() pylsp-rope-0.1.16/test/fixtures/method_with_similar_global_function.py000066400000000000000000000003361457653661300264150ustar00rootroot00000000000000import sys class MyClass: def my_method(self): local_var = 10 print(extracted_method()) print(local_var) print(extracted_method()) def extracted_method(): return sys.stdin.read() pylsp-rope-0.1.16/test/fixtures/method_with_similar_global_variable.py000066400000000000000000000003171457653661300263540ustar00rootroot00000000000000import sys class MyClass: def my_method(self): local_var = 10 print(extracted_variable) print(local_var) print(extracted_variable) extracted_variable = sys.stdin.read() pylsp-rope-0.1.16/test/fixtures/redundant_import.py000066400000000000000000000002231457653661300225060ustar00rootroot00000000000000import sys import os.path import sys def main(): a = sys.stdin.read() b = 20 print(a + b) c = a + b a, b = 30, 40 print(a + b) pylsp-rope-0.1.16/test/fixtures/simple.py000066400000000000000000000001761457653661300204300ustar00rootroot00000000000000import sys def main(): a = int(sys.stdin.read()) b = 20 print(a + b) c = a + b a, b = 30, 40 print(a + b) pylsp-rope-0.1.16/test/fixtures/simple_extract_method.py000066400000000000000000000002661457653661300235220ustar00rootroot00000000000000import sys def main(): a = int(sys.stdin.read()) b = 20 extracted_method(a, b) c = a + b def extracted_method(a, b): print(a + b) a, b = 30, 40 print(a + b) pylsp-rope-0.1.16/test/fixtures/simple_extract_method_with_similar.py000066400000000000000000000003001457653661300262620ustar00rootroot00000000000000import sys def main(): a = int(sys.stdin.read()) b = 20 extracted_method(a, b) c = a + b def extracted_method(a, b): print(a + b) a, b = 30, 40 extracted_method(a, b) pylsp-rope-0.1.16/test/fixtures/simple_extract_variable.py000066400000000000000000000002521457653661300240220ustar00rootroot00000000000000import sys def main(): a = int(sys.stdin.read()) b = 20 extracted_variable = a + b print(extracted_variable) c = a + b a, b = 30, 40 print(a + b) pylsp-rope-0.1.16/test/fixtures/simple_extract_variable_with_similar.py000066400000000000000000000002671457653661300266030ustar00rootroot00000000000000import sys def main(): a = int(sys.stdin.read()) b = 20 extracted_variable = a + b print(extracted_variable) c = extracted_variable a, b = 30, 40 print(a + b) pylsp-rope-0.1.16/test/fixtures/simple_rename.py000066400000000000000000000000661457653661300217550ustar00rootroot00000000000000class Test1(): pass class Test2(Test1): pass pylsp-rope-0.1.16/test/fixtures/simple_rename_extra.py000066400000000000000000000000551457653661300231560ustar00rootroot00000000000000from simple_rename import Test1 x = Test1() pylsp-rope-0.1.16/test/fixtures/simple_rename_extra_result.py000066400000000000000000000001011457653661300245440ustar00rootroot00000000000000from simple_rename import ShouldBeRenamed x = ShouldBeRenamed() pylsp-rope-0.1.16/test/fixtures/simple_rename_result.py000066400000000000000000000001121457653661300233430ustar00rootroot00000000000000class ShouldBeRenamed(): pass class Test2(ShouldBeRenamed): pass pylsp-rope-0.1.16/test/fixtures/undefined_variable.py000066400000000000000000000000561457653661300227420ustar00rootroot00000000000000def foo(): print(undef_var) # noqa: F821 pylsp-rope-0.1.16/test/fixtures/use_function.py000066400000000000000000000001461457653661300216350ustar00rootroot00000000000000def add(a, b): return a + b def main(): a, b = 10, 20 print(f"{a} + {b} = {add(a, b)}") pylsp-rope-0.1.16/test/helpers.py000066400000000000000000000052511457653661300167270ustar00rootroot00000000000000from typing import ( Any, Collection, List, ) from unittest.mock import ANY, call from pylsp.workspace import Workspace, Document from pylsp_rope.typing import ( CodeAction, DocumentContent, DocumentUri, WorkspaceEditWithChanges, TextEdit, ) from test.conftest import read_fixture_file def assert_code_actions_do_not_offer(response: List[CodeAction], command: str) -> None: for action in response: assert action["command"] is not None assert ( action["command"]["command"] != command ), f"CodeAction should not offer {action}" def assert_text_edits(document_edits: List[TextEdit], target: str) -> DocumentContent: new_text = read_fixture_file(target) for change in document_edits: assert change["newText"] in new_text return DocumentContent(new_text) def assert_single_document_edit( edit_request: Any, document: Document ) -> List[TextEdit]: workspace_edit = assert_is_apply_edit_request(edit_request) document_uri: DocumentUri = document.uri assert_modified_documents( workspace_edit, document_uris={document_uri}, ) assert len(workspace_edit["changes"]) == 1 document_edits = workspace_edit["changes"][document_uri] assert isinstance(document_edits, list) return document_edits def assert_is_apply_edit_request(edit_request: Any) -> WorkspaceEditWithChanges: assert edit_request == call( "workspace/applyEdit", { "edit": { "changes": ANY, }, }, ) workspace_edit: WorkspaceEditWithChanges = edit_request[0][1]["edit"] for document_uri, document_edits in workspace_edit["changes"].items(): assert is_document_uri(document_uri) for change in document_edits: assert change == { "range": { "start": {"line": ANY, "character": ANY}, "end": {"line": ANY, "character": ANY}, }, "newText": ANY, } return workspace_edit def is_document_uri(uri: DocumentUri) -> bool: return isinstance(uri, str) and uri.startswith("file://") def assert_modified_documents( workspace_edit: WorkspaceEditWithChanges, document_uris: Collection[DocumentUri], ) -> None: assert workspace_edit["changes"].keys() == set(document_uris) def assert_unmodified_document( workspace_edit: WorkspaceEditWithChanges, document_uri: DocumentUri, ) -> None: assert is_document_uri(document_uri) assert document_uri not in workspace_edit["changes"] def assert_no_execute_command(workspace: Workspace) -> None: workspace._endpoint.request.assert_not_called() pylsp-rope-0.1.16/test/test_commands.py000066400000000000000000000036601457653661300201270ustar00rootroot00000000000000from unittest.mock import patch from pylsp.lsp import MessageType from pylsp_rope import commands, plugin from pylsp_rope.plugin import pylsp_commands, pylsp_execute_command from pylsp_rope.text import Position from test.conftest import create_document from test.helpers import assert_no_execute_command def test_command_registration(config, workspace): commands = pylsp_commands(config, workspace) assert isinstance(commands, list) assert all(isinstance(cmd, str) for cmd in commands) assert all(cmd.startswith("pylsp_rope.") for cmd in commands) def test_command_error_handling(caplog, config, workspace, document): """ pylsp_execute_command should never raise an error when executeCommand(). Instead, we'll show an error message to the user. """ arguments = [ { "document_uri": document.uri, "position": Position(1), }, ] with patch( "pylsp_rope.refactoring.CommandRefactorInline.__call__", side_effect=Exception("some unexpected exception"), ): pylsp_execute_command( config, workspace, command=commands.COMMAND_REFACTOR_INLINE, arguments=arguments, ) workspace._endpoint.notify.assert_called_once_with( "window/showMessage", params={ "type": MessageType.Error, "message": "pylsp-rope: some unexpected exception", }, ) assert "Traceback (most recent call last):" in caplog.text def test_command_nothing_to_modify(config, workspace, document, code_action_context): document = create_document(workspace, "simple.py") command = commands.COMMAND_SOURCE_ORGANIZE_IMPORT arguments = [{"document_uri": document.uri}] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) assert_no_execute_command(workspace) pylsp-rope-0.1.16/test/test_extract.py000066400000000000000000000343501457653661300200000ustar00rootroot00000000000000from pylsp_rope import commands, plugin, typing from pylsp_rope.text import Range from test.conftest import create_document from test.helpers import ( assert_code_actions_do_not_offer, assert_single_document_edit, assert_text_edits, ) def test_extract_variable(config, workspace, code_action_context): document = create_document(workspace, "simple.py") line = 6 start_col = document.lines[line].index("a + b") end_col = document.lines[line].index(")\n") selection = Range((line, start_col), (line, end_col)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Extract variable", "kind": "refactor.extract", "command": { "title": "Extract variable", "command": commands.COMMAND_REFACTOR_EXTRACT_VARIABLE, "arguments": [ { "document_uri": document.uri, "range": selection, "global_": False, "similar": False, } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits(document_edits, target="simple_extract_variable.py") assert "extracted_variable = " in new_text assert new_text.count("a + b") == 3 def test_extract_variable_with_similar(config, workspace, code_action_context): document = create_document(workspace, "simple.py") line = 6 start_col = document.lines[line].index("a + b") end_col = document.lines[line].index(")\n") selection = Range((line, start_col), (line, end_col)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Extract variable including similar statements", "kind": "refactor.extract", "command": { "title": "Extract variable including similar statements", "command": commands.COMMAND_REFACTOR_EXTRACT_VARIABLE, "arguments": [ { "document_uri": document.uri, "range": selection, "global_": False, "similar": True, } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits( document_edits, target="simple_extract_variable_with_similar.py" ) assert "extracted_variable = " in new_text assert new_text.count("a + b") == 2 def test_extract_global_variable(config, workspace, code_action_context): document = create_document(workspace, "method.py") line = 6 start_col = document.lines[line].index("sys.stdin.read()") end_col = document.lines[line].index(")\n") selection = Range((line, start_col), (line, end_col)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Extract global variable", "kind": "refactor.extract", "command": { "title": "Extract global variable", "command": commands.COMMAND_REFACTOR_EXTRACT_VARIABLE, "arguments": [ { "document_uri": document.uri, "range": selection, "global_": True, "similar": False, } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits( document_edits, target="method_with_global_variable.py" ) assert "extracted_variable = " in new_text assert new_text.count("extracted_variable = sys.stdin.read()") == 1 assert new_text.count("sys.stdin.read()") == 2 def test_extract_global_variable_with_similar(config, workspace, code_action_context): document = create_document(workspace, "method.py") line = 6 start_col = document.lines[line].index("sys.stdin.read()") end_col = document.lines[line].index(")\n") selection = Range((line, start_col), (line, end_col)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Extract global variable including similar statements", "kind": "refactor.extract", "command": { "title": "Extract global variable including similar statements", "command": commands.COMMAND_REFACTOR_EXTRACT_VARIABLE, "arguments": [ { "document_uri": document.uri, "range": selection, "global_": True, "similar": True, } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits( document_edits, target="method_with_similar_global_variable.py" ) assert "extracted_variable = " in new_text assert new_text.count("extracted_variable = sys.stdin.read()") == 1 assert new_text.count("sys.stdin.read()") == 1 def test_extract_variable_not_offered_when_selecting_non_expression( config, workspace, document, code_action_context ): line = 6 start_col = document.lines[line].index("print") end_col = document.lines[line].index("+") selection = Range((line, start_col), (line, end_col)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) assert_code_actions_do_not_offer( response, command=commands.COMMAND_REFACTOR_EXTRACT_VARIABLE, ) def test_extract_method(config, workspace, code_action_context): document = create_document(workspace, "simple.py") selection = Range(6) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Extract method", "kind": "refactor.extract", "command": { "title": "Extract method", "command": commands.COMMAND_REFACTOR_EXTRACT_METHOD, "arguments": [ { "document_uri": document.uri, "range": selection, "global_": False, "similar": False, } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits(document_edits, target="simple_extract_method.py") assert "def extracted_method(" in new_text assert new_text.count("print(a + b)") == 2 assert new_text.count("extracted_method(a, b)\n") == 1 def test_extract_method_with_similar(config, workspace, code_action_context): document = create_document(workspace, "simple.py") selection = Range(6) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Extract method including similar statements", "kind": "refactor.extract", "command": { "title": "Extract method including similar statements", "command": commands.COMMAND_REFACTOR_EXTRACT_METHOD, "arguments": [ { "document_uri": document.uri, "range": selection, "global_": False, "similar": True, } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits( document_edits, target="simple_extract_method_with_similar.py" ) assert "def extracted_method(" in new_text assert new_text.count("print(a + b)") == 1 assert new_text.count("extracted_method(a, b)\n") == 2 def test_extract_global_method(config, workspace, code_action_context): document = create_document(workspace, "method.py") line = 6 start_col = document.lines[line].index("sys.stdin.read()") end_col = document.lines[line].index(")\n") selection = Range((line, start_col), (line, end_col)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Extract global method", "kind": "refactor.extract", "command": { "title": "Extract global method", "command": commands.COMMAND_REFACTOR_EXTRACT_METHOD, "arguments": [ { "document_uri": document.uri, "range": selection, "global_": True, "similar": False, } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits( document_edits, target="method_with_global_function.py" ) assert "def extracted_method(" in new_text assert new_text.count("return sys.stdin.read()") == 1 assert new_text.count("sys.stdin.read()") == 2 assert new_text.count("extracted_method()") == 2 def test_extract_method_global_with_similar(config, workspace, code_action_context): document = create_document(workspace, "method.py") line = 6 start_col = document.lines[line].index("sys.stdin.read()") end_col = document.lines[line].index(")\n") selection = Range((line, start_col), (line, end_col)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Extract global method including similar statements", "kind": "refactor.extract", "command": { "title": "Extract global method including similar statements", "command": commands.COMMAND_REFACTOR_EXTRACT_METHOD, "arguments": [ { "document_uri": document.uri, "range": selection, "global_": True, "similar": True, } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits( document_edits, target="method_with_similar_global_function.py" ) assert "def extracted_method(" in new_text assert new_text.count("return sys.stdin.read()") == 1 assert new_text.count("sys.stdin.read()") == 1 assert new_text.count("extracted_method()") == 3 pylsp-rope-0.1.16/test/test_generate.py000066400000000000000000000111731457653661300201160ustar00rootroot00000000000000from pylsp_rope import commands, plugin, typing from pylsp_rope.text import Range from test.conftest import create_document from test.helpers import assert_single_document_edit, assert_text_edits def test_generate_variable(config, workspace, code_action_context): document = create_document(workspace, "undefined_variable.py") line = 1 start_col = end_col = document.lines[line].index("undef_var") selection = Range((line, start_col), (line, end_col)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Generate variable", "kind": "quickfix", "command": { "title": "Generate variable", "command": commands.COMMAND_GENERATE_CODE, "arguments": [ { "document_uri": document.uri, "position": selection["start"], "generate_kind": "variable", } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits(document_edits, target="generate_variable.py") assert "undef_var = None" in new_text def test_generate_function(config, workspace, code_action_context): document = create_document(workspace, "undefined_variable.py") line = 1 start_col = end_col = document.lines[line].index("undef_var") selection = Range((line, start_col), (line, end_col)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Generate function", "kind": "quickfix", "command": { "title": "Generate function", "command": commands.COMMAND_GENERATE_CODE, "arguments": [ { "document_uri": document.uri, "position": selection["start"], "generate_kind": "function", } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits(document_edits, target="generate_function.py") assert "def undef_var():" in new_text def test_generate_class(config, workspace, code_action_context): document = create_document(workspace, "undefined_variable.py") line = 1 start_col = end_col = document.lines[line].index("undef_var") selection = Range((line, start_col), (line, end_col)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Generate class", "kind": "quickfix", "command": { "title": "Generate class", "command": commands.COMMAND_GENERATE_CODE, "arguments": [ { "document_uri": document.uri, "position": selection["start"], "generate_kind": "class", } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits(document_edits, target="generate_class.py") assert "class undef_var(object):" in new_text pylsp-rope-0.1.16/test/test_import_utils.py000066400000000000000000000031731457653661300210570ustar00rootroot00000000000000from pylsp_rope import commands, plugin, typing from pylsp_rope.text import Range from test.conftest import create_document from test.helpers import ( assert_text_edits, assert_single_document_edit, ) def test_organize_import(config, workspace, document, code_action_context): document = create_document(workspace, "redundant_import.py") line = 1 start_col = 0 end_col = 0 selection = Range((line, start_col), (line, end_col)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Organize import", "kind": "source.organizeImports", "command": { "title": "Organize import", "command": commands.COMMAND_SOURCE_ORGANIZE_IMPORT, "arguments": [ { "document_uri": document.uri, } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits(document_edits, target="simple.py") assert document.source.count("import sys") == 2 assert new_text.count("import sys") == 1 pylsp-rope-0.1.16/test/test_inline.py000066400000000000000000000044701457653661300176040ustar00rootroot00000000000000from pylsp_rope import commands, plugin, typing from pylsp_rope.text import Range from test.conftest import create_document from test.helpers import ( assert_text_edits, assert_code_actions_do_not_offer, assert_single_document_edit, ) def test_inline(config, workspace, code_action_context): document = create_document(workspace, "simple_extract_method.py") line = 6 start_col = end_col = document.lines[line].index("extracted_method") selection = Range((line, start_col), (line, end_col)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Inline method/variable/parameter", "kind": "refactor.inline", "command": { "title": "Inline method/variable/parameter", "command": commands.COMMAND_REFACTOR_INLINE, "arguments": [ { "document_uri": document.uri, "position": selection["start"], } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits(document_edits, target="simple.py") assert "extracted_method" not in new_text def test_inline_not_offered_when_selecting_unsuitable_range( config, workspace, code_action_context ): document = create_document(workspace, "simple_extract_variable.py") line = 4 start_col = end_col = document.lines[line].index("stdin") selection = Range((line, start_col), (line, end_col)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) assert_code_actions_do_not_offer( response, command=commands.COMMAND_REFACTOR_INLINE, ) pylsp-rope-0.1.16/test/test_introduce_parameter.py000066400000000000000000000033201457653661300223530ustar00rootroot00000000000000from pylsp_rope import commands, plugin, typing from pylsp_rope.text import Range from test.conftest import create_document from test.helpers import ( assert_code_actions_do_not_offer, assert_single_document_edit, assert_text_edits, ) def test_introduce_parameter(config, workspace, code_action_context): document = create_document(workspace, "simple.py") line = 4 pos = document.lines[line].index("stdin.read()") selection = Range((line, pos), (line, pos)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Introduce parameter", "kind": "refactor", "command": { "title": "Introduce parameter", "command": commands.COMMAND_INTRODUCE_PARAMETER, "arguments": [ { "document_uri": document.uri, "position": selection["start"], } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits(document_edits, target="introduce_parameter.py") assert "new_parameter=sys.stdin" in new_text assert "new_parameter.read()" in new_text pylsp-rope-0.1.16/test/test_local_to_field.py000066400000000000000000000044671457653661300212730ustar00rootroot00000000000000from pylsp_rope import commands, plugin, typing from pylsp_rope.text import Range from test.conftest import create_document from test.helpers import ( assert_text_edits, assert_code_actions_do_not_offer, assert_single_document_edit, ) def test_local_to_field(config, workspace, code_action_context): document = create_document(workspace, "method.py") line = 5 start_col = end_col = document.lines[line].index("local_var") selection = Range((line, start_col), (line, end_col)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Convert local variable to field", "kind": "refactor.rewrite", "command": { "title": "Convert local variable to field", "command": commands.COMMAND_REFACTOR_LOCAL_TO_FIELD, "arguments": [ { "document_uri": document.uri, "position": selection["start"], } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits(document_edits, target="method_local_to_field.py") assert "extracted_method" not in new_text def test_local_to_field_not_offered_when_selecting_unsuitable_range( config, workspace, code_action_context ): document = create_document(workspace, "method.py") line = 6 start_col = end_col = document.lines[line].index("stdin") selection = Range((line, start_col), (line, end_col)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) assert_code_actions_do_not_offer( response, command=commands.COMMAND_REFACTOR_INLINE, ) pylsp-rope-0.1.16/test/test_lsp_diff.py000066400000000000000000000046751457653661300201230ustar00rootroot00000000000000from pylsp_rope.lsp_diff import _difflib_ops_to_text_edit_ops, lsp_diff from test.conftest import create_document def test_lsp_diff(workspace): expected = [ { "range": { "start": {"line": 2, "character": 0}, "end": {"line": 3, "character": 0}, }, "newText": "", }, { "range": { "start": {"line": 4, "character": 0}, "end": {"line": 5, "character": 0}, }, "newText": 'print("world")\n', }, { "range": { "start": {"line": 15, "character": 0}, "end": {"line": 16, "character": 0}, }, "newText": ' os.path.join("world", roses)\n', }, ] old_document = create_document(workspace, "many_changes.py") new_document = create_document(workspace, "many_changes_inlined.py") changes = list(lsp_diff(old_document.lines, new_document.lines)) assert changes == expected def test_difflib_ops_to_text_edit_ops_insert(workspace): expected = { "range": { "start": {"line": 5, "character": 0}, "end": {"line": 5, "character": 0}, }, "newText": 'are = "here"\nred = "here"\n', } new_document = create_document(workspace, "many_changes_inlined.py") difflib_ops = ("insert", 5, 5, 6, 8) text_edit_ops = _difflib_ops_to_text_edit_ops(difflib_ops, new_document.lines) assert text_edit_ops == expected def test_difflib_ops_to_text_edit_ops_delete(workspace): expected = { "range": { "start": {"line": 2, "character": 0}, "end": {"line": 3, "character": 0}, }, "newText": "", } new_document = create_document(workspace, "many_changes_inlined.py") difflib_ops = ("delete", 2, 3, 2, 2) text_edit_ops = _difflib_ops_to_text_edit_ops(difflib_ops, new_document.lines) assert text_edit_ops == expected def test_difflib_ops_to_text_edit_ops_replace(workspace): expected = { "range": { "start": {"line": 4, "character": 0}, "end": {"line": 5, "character": 0}, }, "newText": 'print("world")\n', } new_document = create_document(workspace, "many_changes_inlined.py") difflib_ops = ("replace", 4, 5, 3, 4) text_edit_ops = _difflib_ops_to_text_edit_ops(difflib_ops, new_document.lines) assert text_edit_ops == expected pylsp-rope-0.1.16/test/test_method_to_method_object.py000066400000000000000000000044711457653661300231770ustar00rootroot00000000000000from pylsp_rope import commands, plugin, typing from pylsp_rope.text import Range from test.conftest import create_document from test.helpers import ( assert_text_edits, assert_code_actions_do_not_offer, assert_single_document_edit, ) def test_method_to_method_object(config, workspace, code_action_context): document = create_document(workspace, "function.py") line = 0 pos = document.lines[line].index("def add") + 4 selection = Range((line, pos), (line, pos)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "To method object", "kind": "refactor.rewrite", "command": { "title": "To method object", "command": commands.COMMAND_REFACTOR_METHOD_TO_METHOD_OBJECT, "arguments": [ { "document_uri": document.uri, "position": selection["start"], } ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args document_edits = assert_single_document_edit(edit_request, document) new_text = assert_text_edits(document_edits, target="method_object.py") assert "class NewMethodObject(object)" in new_text assert "NewMethodObject(a, b)()" in new_text def test_method_to_method_object_not_offered_when_selecting_unsuitable_range( config, workspace, code_action_context ): document = create_document(workspace, "function.py") line = 1 pos = document.lines[line].index("return") selection = Range((line, pos), (line, pos)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) assert_code_actions_do_not_offer( response, command=commands.COMMAND_REFACTOR_INLINE, ) pylsp-rope-0.1.16/test/test_project.py000066400000000000000000000044711457653661300177750ustar00rootroot00000000000000from rope.refactor import inline from pylsp_rope.project import ( get_project, get_resource, rope_changeset_to_workspace_edit, ) from pylsp_rope.typing import ( is_workspace_edit_with_changes, is_workspace_edit_with_document_changes, ) from test.conftest import create_document EXPECTED_EDITS = [ { "range": { "start": {"line": 2, "character": 0}, "end": {"line": 3, "character": 0}, }, "newText": "", }, { "range": { "start": {"line": 4, "character": 0}, "end": {"line": 5, "character": 0}, }, "newText": 'print("world")\n', }, { "range": { "start": {"line": 15, "character": 0}, "end": {"line": 16, "character": 0}, }, "newText": ' os.path.join("world", roses)\n', }, ] def test_rope_changeset_to_workspace_changeset_changes(workspace): document = create_document(workspace, "many_changes.py") rope_changeset = get_rope_changeset(workspace, document) workspace_edit = rope_changeset_to_workspace_edit( workspace, rope_changeset, workspace_edit_format=["changes"], ) assert is_workspace_edit_with_changes(workspace_edit) assert workspace_edit["changes"] == { document.uri: EXPECTED_EDITS, } def test_rope_changeset_to_workspace_changeset_document_changes(workspace): document = create_document(workspace, "many_changes.py") rope_changeset = get_rope_changeset(workspace, document) workspace_edit = rope_changeset_to_workspace_edit( workspace, rope_changeset, workspace_edit_format=["documentChanges"], ) assert is_workspace_edit_with_document_changes(workspace_edit) assert workspace_edit["documentChanges"] == [ { "textDocument": { "uri": document.uri, "version": None, }, "edits": EXPECTED_EDITS, }, ] def get_rope_changeset(workspace, document): _, resource = get_resource(workspace, document.uri) offset = document.source.index("hello = ") refactoring = inline.create_inline( project=get_project(workspace), resource=resource, offset=offset, ) rope_changeset = refactoring.get_changes() return rope_changeset pylsp-rope-0.1.16/test/test_rename.py000066400000000000000000000046431457653661300175770ustar00rootroot00000000000000import pytest from pylsp_rope import typing from pylsp_rope.plugin import pylsp_rename from pylsp_rope.text import Position from test.conftest import create_document from test.helpers import assert_text_edits, assert_modified_documents @pytest.fixture(autouse=True) def enable_pylsp_rope_rename_plugin(config): config._plugin_settings["plugins"]["pylsp_rope"] = {"rename": True} return config def test_rope_rename(config, workspace) -> None: document = create_document(workspace, "simple_rename.py") extra_document = create_document(workspace, "simple_rename_extra.py") line = 0 pos = document.lines[line].index("Test1") position = Position(line, pos) response: typing.SimpleWorkspaceEdit = pylsp_rename(config, workspace, document, position, "ShouldBeRenamed") assert len(response.keys()) == 1 assert_modified_documents(response, {document.uri, extra_document.uri}) new_text = assert_text_edits( response["changes"][document.uri], target="simple_rename_result.py" ) assert "class ShouldBeRenamed()" in new_text assert "class Test2(ShouldBeRenamed)" in new_text new_text = assert_text_edits( response["changes"][extra_document.uri], target="simple_rename_extra_result.py" ) assert "from simple_rename import ShouldBeRenamed" in new_text assert "x = ShouldBeRenamed()" in new_text def test_rope_rename_disabled(config, workspace) -> None: document = create_document(workspace, "simple_rename.py") extra_document = create_document(workspace, "simple_rename_extra.py") line = 0 pos = document.lines[line].index("Test1") position = Position(line, pos) plugin_settings = config.plugin_settings("pylsp_rope", document.uri) plugin_settings["rename"] = False response: typing.SimpleWorkspaceEdit = pylsp_rename(config, workspace, document, position, "ShouldBeRenamed") assert response is None def test_rope_rename_missing_key(config, workspace) -> None: document = create_document(workspace, "simple_rename.py") extra_document = create_document(workspace, "simple_rename_extra.py") line = 0 pos = document.lines[line].index("Test1") position = Position(line, pos) plugin_settings = config.plugin_settings("pylsp_rope", document.uri) del plugin_settings["rename"] response: typing.SimpleWorkspaceEdit = pylsp_rename(config, workspace, document, position, "ShouldBeRenamed") assert response is None pylsp-rope-0.1.16/test/test_usefunction.py000066400000000000000000000073651457653661300206760ustar00rootroot00000000000000from pylsp_rope import plugin, commands, typing from pylsp_rope.text import Range from test.conftest import create_document from test.helpers import ( assert_text_edits, assert_is_apply_edit_request, assert_modified_documents, assert_unmodified_document, ) def test_use_function_globally(config, workspace, code_action_context): document = create_document(workspace, "function.py") document2 = create_document(workspace, "method_object.py") line = 0 pos = document.lines[line].index("def add") + 4 selection = Range((line, pos), (line, pos)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Use function", "kind": "refactor", "command": { "title": "Use function", "command": commands.COMMAND_REFACTOR_USE_FUNCTION, "arguments": [ { "document_uri": document.uri, "position": selection["start"], }, ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args workspace_edit = assert_is_apply_edit_request(edit_request) assert_modified_documents(workspace_edit, {document.uri, document2.uri}) new_text = assert_text_edits( workspace_edit["changes"][document.uri], target="use_function.py" ) assert "{add(a, b)}" in new_text new_text = assert_text_edits( workspace_edit["changes"][document2.uri], target="method_object_use_function.py" ) assert "import function" in new_text assert "{function.add(a, b)}" in new_text def test_use_function_in_current_file(config, workspace, code_action_context): document = create_document(workspace, "function.py") document2 = create_document(workspace, "method_object.py") line = 0 pos = document.lines[line].index("def add") + 4 selection = Range((line, pos), (line, pos)) response = plugin.pylsp_code_actions( config=config, workspace=workspace, document=document, range=selection, context=code_action_context, ) expected: typing.CodeAction = { "title": "Use function for current file only", "kind": "refactor", "command": { "title": "Use function for current file only", "command": commands.COMMAND_REFACTOR_USE_FUNCTION, "arguments": [ { "document_uri": document.uri, "position": selection["start"], "documents": [document.uri], }, ], }, } assert expected in response assert expected["command"] is not None command = expected["command"]["command"] arguments = expected["command"]["arguments"] response = plugin.pylsp_execute_command( config=config, workspace=workspace, command=command, arguments=arguments, ) edit_request = workspace._endpoint.request.call_args workspace_edit = assert_is_apply_edit_request(edit_request) assert_modified_documents(workspace_edit, {document.uri}) new_text = assert_text_edits( workspace_edit["changes"][document.uri], target="use_function.py" ) assert "{add(a, b)}" in new_text assert_unmodified_document(workspace_edit, document2.uri)