def test_jedi_rename(tmp_workspace, config): # pylint: disable=redefined-outer-name # rename the `Test1` class position = {'line': 0, 'character': 6} DOC_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC_NAME)) doc = Document(DOC_URI, tmp_workspace) result = pylsp_rename(config, tmp_workspace, doc, position, 'ShouldBeRenamed') assert len(result.keys()) == 1 changes = result.get('documentChanges') assert len(changes) == 2 assert changes[0]['textDocument']['uri'] == doc.uri assert changes[0]['textDocument']['version'] == doc.version assert changes[0].get('edits') == [{ 'range': { 'start': { 'line': 0, 'character': 0 }, 'end': { 'line': 5, 'character': 0 }, }, 'newText': 'class ShouldBeRenamed():\n pass\n\nclass Test2(ShouldBeRenamed):\n pass\n', }] path = os.path.join(tmp_workspace.root_path, DOC_NAME_EXTRA) uri_extra = uris.from_fs_path(path) assert changes[1]['textDocument']['uri'] == uri_extra # This also checks whether documents not yet added via textDocument/didOpen # but that do need to be renamed in the project have a `null` version # number. assert changes[1]['textDocument']['version'] is None expected = 'from test1 import ShouldBeRenamed\nx = ShouldBeRenamed()\n' if os.name == 'nt': # The .write method in the temp_workspace_factory functions writes # Windows-style line-endings. expected = expected.replace('\n', '\r\n') assert changes[1].get('edits') == [{ 'range': { 'start': { 'line': 0, 'character': 0 }, 'end': { 'line': 2, 'character': 0 } }, 'newText': expected }]
def pylsp_rename(config, workspace, document, position, new_name): # pylint: disable=unused-argument log.debug('Executing rename of %s to %s', document.word_at_position(position), new_name) kwargs = _utils.position_to_jedi_linecolumn(document, position) kwargs['new_name'] = new_name try: refactoring = document.jedi_script().rename(**kwargs) except NotImplementedError as exc: raise Exception('No support for renaming in Python 2/3.5 with Jedi. ' 'Consider using the rope_rename plugin instead') from exc log.debug('Finished rename: %s', refactoring.get_diff()) changes = [] for file_path, changed_file in refactoring.get_changed_files().items(): uri = uris.from_fs_path(str(file_path)) doc = workspace.get_maybe_document(uri) changes.append({ 'textDocument': { 'uri': uri, 'version': doc.version if doc else None }, 'edits': [ { 'range': { 'start': {'line': 0, 'character': 0}, 'end': { 'line': _num_lines(changed_file.get_new_code()), 'character': 0, }, }, 'newText': changed_file.get_new_code(), } ], }) return {'documentChanges': changes}
def test_root_project_with_no_setup_py(pylsp): """Default to workspace root.""" workspace_root = pylsp.workspace.root_path test_uri = uris.from_fs_path(os.path.join(workspace_root, 'hello/test.py')) pylsp.workspace.put_document(test_uri, 'assert True') test_doc = pylsp.workspace.get_document(test_uri) assert workspace_root in test_doc.sys_path()
def test_references(tmp_workspace): # pylint: disable=redefined-outer-name # Over 'Test1' in class Test1(): position = {'line': 0, 'character': 8} DOC1_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC1_NAME)) doc1 = Document(DOC1_URI, tmp_workspace) refs = pylsp_references(doc1, position) # Definition, the import and the instantiation assert len(refs) == 3 # Briefly check excluding the definitions (also excludes imports, only counts uses) no_def_refs = pylsp_references(doc1, position, exclude_declaration=True) assert len(no_def_refs) == 1 # Make sure our definition is correctly located doc1_ref = [u for u in refs if u['uri'] == DOC1_URI][0] assert doc1_ref['range']['start'] == {'line': 0, 'character': 6} assert doc1_ref['range']['end'] == {'line': 0, 'character': 11} # Make sure our import is correctly located doc2_import_ref = [u for u in refs if u['uri'] != DOC1_URI][0] assert doc2_import_ref['range']['start'] == {'line': 0, 'character': 18} assert doc2_import_ref['range']['end'] == {'line': 0, 'character': 23} doc2_usage_ref = [u for u in refs if u['uri'] != DOC1_URI][1] assert doc2_usage_ref['range']['start'] == {'line': 3, 'character': 4} assert doc2_usage_ref['range']['end'] == {'line': 3, 'character': 9}
def test_flake8_multiline(workspace): config_str = r"""[flake8] exclude = blah/, file_2.py """ doc_str = "print('hi')\nimport os\n" doc_uri = uris.from_fs_path( os.path.join(workspace.root_path, "blah/__init__.py")) workspace.put_document(doc_uri, doc_str) flake8_settings = get_flake8_cfg_settings(workspace, config_str) assert "exclude" in flake8_settings assert len(flake8_settings["exclude"]) == 2 with patch('pylsp.plugins.flake8_lint.Popen') as popen_mock: mock_instance = popen_mock.return_value mock_instance.communicate.return_value = [bytes(), bytes()] doc = workspace.get_document(doc_uri) flake8_lint.pylsp_lint(workspace, doc) call_args = popen_mock.call_args[0][0] assert call_args == ["flake8", "-", "--exclude=blah/,file_2.py"] os.unlink(os.path.join(workspace.root_path, "setup.cfg"))
def test_rope_rename(tmp_workspace, config): # pylint: disable=redefined-outer-name position = {"line": 0, "character": 6} DOC_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC_NAME)) doc = Document(DOC_URI, tmp_workspace) result = pylsp_rename(config, tmp_workspace, doc, position, "ShouldBeRenamed") assert len(result.keys()) == 1 changes = result.get("documentChanges") assert len(changes) == 1 changes = changes[0] # Note that this test differs from test_jedi_rename, because rope does not # seem to modify files that haven't been opened with textDocument/didOpen. assert changes.get("edits") == [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 5, "character": 0 }, }, "newText": "class ShouldBeRenamed():\n pass\n\nclass Test2(ShouldBeRenamed):\n pass\n", }]
def temp_document(doc_text, workspace): with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp_file: name = temp_file.name temp_file.write(doc_text) doc = Document(uris.from_fs_path(name), workspace) return name, doc
def test_per_file_caching(config, workspace): # Ensure that diagnostics are cached per-file. with temp_document(DOC, workspace) as doc: assert pylint_lint.pylsp_lint(config, doc, True) assert not pylint_lint.pylsp_lint( config, Document(uris.from_fs_path(__file__), workspace), False)
def test_flake8_per_file_ignores(workspace): config_str = r"""[flake8] ignores = F403 per-file-ignores = **/__init__.py:F401,E402 test_something.py:E402, exclude = file_1.py file_2.py """ doc_str = "print('hi')\nimport os\n" doc_uri = uris.from_fs_path( os.path.join(workspace.root_path, "blah/__init__.py")) workspace.put_document(doc_uri, doc_str) flake8_settings = get_flake8_cfg_settings(workspace, config_str) assert "perFileIgnores" in flake8_settings assert len(flake8_settings["perFileIgnores"]) == 2 assert "exclude" in flake8_settings assert len(flake8_settings["exclude"]) == 2 doc = workspace.get_document(doc_uri) res = flake8_lint.pylsp_lint(workspace, doc) assert not res os.unlink(os.path.join(workspace.root_path, "setup.cfg"))
def temp_document(doc_text, workspace): try: with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp_file: name = temp_file.name temp_file.write(doc_text) yield Document(uris.from_fs_path(name), workspace) finally: os.remove(name)
def test_basic(workspace, config): doc = Document(uris.from_fs_path(str(data / "file.py")), workspace) diagnostics = pyls_lint(config, doc) assert diagnostics == [ build_diagnostic("foo", (7, 4), (7, 7), "deprecated at some point"), build_diagnostic("imported", (9, 0), (9, 8), "test reason"), ]
def test_pycodestyle_config(workspace): """ Test that we load config files properly. Config files are loaded in the following order: tox.ini pep8.cfg setup.cfg pycodestyle.cfg Each overriding the values in the last. These files are first looked for in the current document's directory and then each parent directory until any one is found terminating at the workspace root. If any section called 'pycodestyle' exists that will be solely used and any config in a 'pep8' section will be ignored """ doc_uri = uris.from_fs_path(os.path.join(workspace.root_path, 'test.py')) workspace.put_document(doc_uri, DOC) doc = workspace.get_document(doc_uri) # Make sure we get a warning for 'indentation contains tabs' diags = pycodestyle_lint.pylsp_lint(workspace, doc) assert [d for d in diags if d['code'] == 'W191'] content = { 'setup.cfg': ('[pycodestyle]\nignore = W191, E201, E128', True), 'tox.ini': ('', False) } for conf_file, (content, working) in list(content.items()): # Now we'll add config file to ignore it with open(os.path.join(workspace.root_path, conf_file), 'w+', encoding='utf-8') as f: f.write(content) workspace._config.settings.cache_clear() # And make sure we don't get any warnings diags = pycodestyle_lint.pylsp_lint(workspace, doc) assert len([d for d in diags if d['code'] == 'W191']) == (0 if working else 1) assert len([d for d in diags if d['code'] == 'E201']) == (0 if working else 1) assert [d for d in diags if d['code'] == 'W391'] os.unlink(os.path.join(workspace.root_path, conf_file)) # Make sure we can ignore via the PYLS config as well workspace._config.update( {'plugins': { 'pycodestyle': { 'ignore': ['W191', 'E201'] } }}) # And make sure we only get one warning diags = pycodestyle_lint.pylsp_lint(workspace, doc) assert not [d for d in diags if d['code'] == 'W191'] assert not [d for d in diags if d['code'] == 'E201'] assert [d for d in diags if d['code'] == 'W391']
def pylsp(tmpdir): """ Return an initialized python LS """ ls = PythonLSPServer(StringIO, StringIO) ls.m_initialize(processId=1, rootUri=uris.from_fs_path(str(tmpdir)), initializationOptions={}) return ls
def test_config_file(tmpdir, workspace): # a config file in the same directory as the source file will be used conf = tmpdir.join('.style.yapf') conf.write('[style]\ncolumn_limit = 14') src = tmpdir.join('test.py') doc = Document(uris.from_fs_path(src.strpath), workspace, DOC) # A was split on multiple lines because of column_limit from config file assert pylsp_format_document(doc)[0]['newText'] == "A = [\n 'h', 'w',\n 'a'\n]\n\nB = ['h', 'w']\n"
def pylsp_w_workspace_folders(tmpdir): """ Return an initialized python LS """ ls = PythonLSPServer(StringIO, StringIO) folder1 = tmpdir.mkdir('folder1') folder2 = tmpdir.mkdir('folder2') ls.m_initialize(processId=1, rootUri=uris.from_fs_path(str(folder1)), initializationOptions={}, workspaceFolders=[{ 'uri': uris.from_fs_path(str(folder1)), 'name': 'folder1' }, { 'uri': uris.from_fs_path(str(folder2)), 'name': 'folder2' }]) workspace_folders = [folder1, folder2] return (ls, workspace_folders)
def test_references_builtin(tmp_workspace): # pylint: disable=redefined-outer-name # Over 'UnicodeError': position = {'line': 4, 'character': 7} doc2_uri = uris.from_fs_path(os.path.join(str(tmp_workspace.root_path), DOC2_NAME)) doc2 = Document(doc2_uri, tmp_workspace) refs = pylsp_references(doc2, position) assert len(refs) >= 1 expected = {'start': {'line': 4, 'character': 7}, 'end': {'line': 4, 'character': 19}} ranges = [r['range'] for r in refs] assert expected in ranges
def test_non_root_project(pylsp, metafiles): repo_root = os.path.join(pylsp.workspace.root_path, 'repo-root') os.mkdir(repo_root) project_root = os.path.join(repo_root, 'project-root') os.mkdir(project_root) for metafile in metafiles: with open(os.path.join(project_root, metafile), 'w+') as f: f.write('# ' + metafile) test_uri = uris.from_fs_path(os.path.join(project_root, 'hello/test.py')) pylsp.workspace.put_document(test_uri, 'assert True') test_doc = pylsp.workspace.get_document(test_uri) assert project_root in test_doc.sys_path()
def test_multiple_workspaces(tmpdir, last_diagnostics_monkeypatch): DOC_SOURCE = """ def foo(): return unreachable = 1 """ DOC_ERR_MSG = "Statement is unreachable" # Initialize two workspace folders. folder1 = tmpdir.mkdir("folder1") ws1 = Workspace(uris.from_fs_path(str(folder1)), Mock()) ws1._config = Config(ws1.root_uri, {}, 0, {}) folder2 = tmpdir.mkdir("folder2") ws2 = Workspace(uris.from_fs_path(str(folder2)), Mock()) ws2._config = Config(ws2.root_uri, {}, 0, {}) # Create configuration file for workspace folder 1. mypy_config = folder1.join("mypy.ini") mypy_config.write( "[mypy]\nwarn_unreachable = True\ncheck_untyped_defs = True") # Initialize settings for both folders. plugin.pylsp_settings(ws1._config) plugin.pylsp_settings(ws2._config) # Test document in workspace 1 (uses mypy.ini configuration). doc1 = Document(DOC_URI, ws1, DOC_SOURCE) diags = plugin.pylsp_lint(ws1._config, ws1, doc1, is_saved=False) assert len(diags) == 1 diag = diags[0] assert diag["message"] == DOC_ERR_MSG # Test document in workspace 2 (without mypy.ini configuration) doc2 = Document(DOC_URI, ws2, DOC_SOURCE) diags = plugin.pylsp_lint(ws2._config, ws2, doc2, is_saved=False) assert len(diags) == 0
def test_document_path_completions(tmpdir, workspace_other_root_path): # Create a dummy module out of the workspace's root_path and try to get # completions for it in another file placed next to it. module_content = ''' def foo(): pass ''' p = tmpdir.join("mymodule.py") p.write(module_content) # Content of doc to test completion doc_content = """import mymodule mymodule.f""" doc_path = str(tmpdir) + os.path.sep + 'myfile.py' doc_uri = uris.from_fs_path(doc_path) doc = Document(doc_uri, workspace_other_root_path, doc_content) com_position = {'line': 1, 'character': 10} completions = pylsp_jedi_completions(doc._config, doc, com_position) assert completions[0]['label'] == 'foo()'
def test_per_file_ignores_alternative_syntax(workspace): config_str = r"""[flake8] per-file-ignores = **/__init__.py:F401,E402 """ doc_str = "print('hi')\nimport os\n" doc_uri = uris.from_fs_path( os.path.join(workspace.root_path, "blah/__init__.py")) workspace.put_document(doc_uri, doc_str) flake8_settings = get_flake8_cfg_settings(workspace, config_str) assert "perFileIgnores" in flake8_settings assert len(flake8_settings["perFileIgnores"]) == 2 doc = workspace.get_document(doc_uri) res = flake8_lint.pylsp_lint(workspace, doc) assert not res os.unlink(os.path.join(workspace.root_path, "setup.cfg"))
def pylsp_rename(config, workspace, document, position, new_name): rope_config = config.settings(document_path=document.path).get('rope', {}) rope_project = workspace._rope_project_builder(rope_config) rename = Rename( rope_project, libutils.path_to_resource(rope_project, document.path), document.offset_at_position(position) ) log.debug("Executing rename of %s to %s", document.word_at_position(position), new_name) changeset = rename.get_changes(new_name, in_hierarchy=True, docs=True) log.debug("Finished rename: %s", changeset.changes) changes = [] for change in changeset.changes: uri = uris.from_fs_path(change.resource.path) doc = workspace.get_maybe_document(uri) changes.append({ 'textDocument': { 'uri': uri, 'version': doc.version if doc else None }, 'edits': [ { 'range': { 'start': {'line': 0, 'character': 0}, 'end': { 'line': _num_lines(change.resource), 'character': 0, }, }, 'newText': change.new_contents, } ] }) return {'documentChanges': changes}
def test_lint_free_pylint(config, workspace): # Can't use temp_document because it might give us a file that doesn't # match pylint's naming requirements. We should be keeping this file clean # though, so it works for a test of an empty lint. assert not pylint_lint.pylsp_lint( config, Document(uris.from_fs_path(__file__), workspace), True)
def test_get_missing_document(tmpdir, pylsp): source = 'TEXT' doc_path = tmpdir.join("test_document.py") doc_path.write(source) doc_uri = uris.from_fs_path(str(doc_path)) assert pylsp.workspace.get_document(doc_uri).source == 'TEXT'
# Copyright 2017 Palantir Technologies, Inc. import os import pathlib import pytest from pylsp import uris DOC_URI = uris.from_fs_path(__file__) def path_as_uri(path): return pathlib.Path(os.path.abspath(path)).as_uri() def test_local(pylsp): """ Since the workspace points to the test directory """ assert pylsp.workspace.is_local() def test_put_document(pylsp): pylsp.workspace.put_document(DOC_URI, 'content') assert DOC_URI in pylsp.workspace._docs def test_get_document(pylsp): pylsp.workspace.put_document(DOC_URI, 'TEXT') assert pylsp.workspace.get_document(DOC_URI).source == 'TEXT' def test_get_missing_document(tmpdir, pylsp): source = 'TEXT'
def write_doc(text): temp_file.write(text) temp_file.close() doc = Document(uris.from_fs_path(temp_file.name), workspace) return doc
def workspace(tmpdir, config): ws = Workspace(uris.from_fs_path(str(tmpdir)), Mock()) ws._config = config return ws
def config(tmpdir): config = Config(uris.from_fs_path(str(tmpdir)), {}, 0, {}) config.update(pyls_settings()) return config
# Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. import os from pylsp import lsp, uris from pylsp.workspace import Document from pylsp.plugins import pydocstyle_lint DOC_URI = uris.from_fs_path( os.path.join(os.path.dirname(__file__), "pydocstyle.py")) TEST_DOC_URI = uris.from_fs_path(__file__) DOC = """import sys def hello(): \tpass import json """ def test_pydocstyle(config, workspace): doc = Document(DOC_URI, workspace, DOC) diags = pydocstyle_lint.pylsp_lint(config, doc) assert all(d['source'] == 'pydocstyle' for d in diags) # One we're expecting is: assert diags[0] == { 'code': 'D100', 'message': 'D100: Missing docstring in public module',
def workspace(tmpdir): """Return a workspace.""" ws = Workspace(uris.from_fs_path(str(tmpdir)), Mock()) ws._config = Config(ws.root_uri, {}, 0, {}) return ws
def workspace_other_root_path(tmpdir): """Return a workspace with a root_path other than tmpdir.""" ws_path = str(tmpdir.mkdir('test123').mkdir('test456')) ws = Workspace(uris.from_fs_path(ws_path), Mock()) ws._config = Config(ws.root_uri, {}, 0, {}) return ws