diff --git a/external-deps/python-lsp-server/.github/workflows/static.yml b/external-deps/python-lsp-server/.github/workflows/static.yml index 04cd2a8f4f5..d6aec6a1d11 100644 --- a/external-deps/python-lsp-server/.github/workflows/static.yml +++ b/external-deps/python-lsp-server/.github/workflows/static.yml @@ -30,7 +30,7 @@ jobs: # errors first python-version: '3.6' architecture: 'x64' - - run: python -m pip install --upgrade pip setuptools + - run: python -m pip install --upgrade pip setuptools jsonschema - run: pip install -e .[pylint,pycodestyle,pyflakes] - name: Pylint checks run: pylint pylsp test @@ -38,3 +38,9 @@ jobs: run: pycodestyle pylsp test - name: Pyflakes checks run: pyflakes pylsp test + - name: Validate JSON schema + run: jsonschema pylsp/config/schema.json + - name: Ensure JSON schema and Markdown docs are in sync + run: | + python scripts/jsonschema2md.py pylsp/config/schema.json EXPECTED_CONFIGURATION.md + diff EXPECTED_CONFIGURATION.md CONFIGURATION.md diff --git a/external-deps/python-lsp-server/.gitignore b/external-deps/python-lsp-server/.gitignore index ac609b32350..4f47251da6f 100644 --- a/external-deps/python-lsp-server/.gitignore +++ b/external-deps/python-lsp-server/.gitignore @@ -3,6 +3,9 @@ __pycache__/ *.py[cod] *$py.class +# Mypy cache +.mypy_cache/ + # IntelliJ *.iml *.ipr diff --git a/external-deps/python-lsp-server/.gitrepo b/external-deps/python-lsp-server/.gitrepo index f9c1a9e091f..2aea2122134 100644 --- a/external-deps/python-lsp-server/.gitrepo +++ b/external-deps/python-lsp-server/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = https://github.com/python-lsp/python-lsp-server.git branch = develop - commit = 22a746e5e1dc1c075360242cfd27813687f1b142 - parent = 04e393bcea8ee2968bab4ff9a861300c3f802b20 + commit = 58b229b520b7cb78196115d0366e94596242dc4c + parent = f8e5f56cc0515580006c349ebe4fe7e16b411284 method = merge cmdver = 0.4.3 diff --git a/external-deps/python-lsp-server/CHANGELOG.md b/external-deps/python-lsp-server/CHANGELOG.md index 836fa0e91c5..b862604de6c 100644 --- a/external-deps/python-lsp-server/CHANGELOG.md +++ b/external-deps/python-lsp-server/CHANGELOG.md @@ -1,3 +1,32 @@ +## Version 1.1.0 (2021-06-25) + +### New features + +* Add support for flake8 per-file-ignores +* Add --version CLI argument and return version in InitializeResult + +### Issues Closed + +* [Issue 30](https://github.com/python-lsp/python-lsp-server/issues/30) - pylsp_document_symbols raising TypeError from os.path.samefile ([PR 31](https://github.com/python-lsp/python-lsp-server/pull/31) by [@douglasdavis](https://github.com/douglasdavis)) +* [Issue 19](https://github.com/python-lsp/python-lsp-server/issues/19) - Linter and tests are failing on due to new "consider-using-with" ([PR 20](https://github.com/python-lsp/python-lsp-server/pull/20) by [@krassowski](https://github.com/krassowski)) + +In this release 2 issues were closed. + +### Pull Requests Merged + +* [PR 44](https://github.com/python-lsp/python-lsp-server/pull/44) - Add --version CLI argument and return version in InitializeResult, by [@nemethf](https://github.com/nemethf) +* [PR 42](https://github.com/python-lsp/python-lsp-server/pull/42) - Fix local timezone, by [@e-kwsm](https://github.com/e-kwsm) +* [PR 38](https://github.com/python-lsp/python-lsp-server/pull/38) - Handling list merge in _utils.merge_dicts()., by [@GaetanLepage](https://github.com/GaetanLepage) +* [PR 32](https://github.com/python-lsp/python-lsp-server/pull/32) - PR: Update third-party plugins in README, by [@haplo](https://github.com/haplo) +* [PR 31](https://github.com/python-lsp/python-lsp-server/pull/31) - Catch a TypeError from os.path.samefile, by [@douglasdavis](https://github.com/douglasdavis) ([30](https://github.com/python-lsp/python-lsp-server/issues/30)) +* [PR 28](https://github.com/python-lsp/python-lsp-server/pull/28) - Add support for flake8 per-file-ignores, by [@brandonwillard](https://github.com/brandonwillard) +* [PR 20](https://github.com/python-lsp/python-lsp-server/pull/20) - PR: Address pylint's "consider-using-with" warnings, by [@krassowski](https://github.com/krassowski) ([19](https://github.com/python-lsp/python-lsp-server/issues/19)) +* [PR 18](https://github.com/python-lsp/python-lsp-server/pull/18) - Fix Jedi type map (use types offered by modern Jedi), by [@krassowski](https://github.com/krassowski) + +In this release 8 pull requests were closed. + +---- + ## Version 1.0.1 (2021-04-22) ### Issues Closed @@ -13,6 +42,7 @@ In this release 1 issue was closed. In this release 2 pull requests were closed. +---- ## Version 1.0.0 (2021/04/14) diff --git a/external-deps/python-lsp-server/CONFIGURATION.md b/external-deps/python-lsp-server/CONFIGURATION.md new file mode 100644 index 00000000000..96559c7581e --- /dev/null +++ b/external-deps/python-lsp-server/CONFIGURATION.md @@ -0,0 +1,54 @@ +# Python Language Server Configuration +This server can be configured using `workspace/didChangeConfiguration` method. Each configuration option is described below: + +| **Configuration Key** | **Type** | **Description** | **Default** +|----|----|----|----| +| `pylsp.configurationSources` | `array` of unique `string` items | List of configuration sources to use. | `["pycodestyle"]` | +| `pylsp.plugins.jedi.extra_paths` | `array` | Define extra paths for jedi.Script. | `[]` | +| `pylsp.plugins.jedi.env_vars` | `object` | Define environment variables for jedi.Script and Jedi.names. | `null` | +| `pylsp.plugins.jedi.environment` | `string` | Define environment for jedi.Script and Jedi.names. | `null` | +| `pylsp.plugins.jedi_completion.enabled` | `boolean` | Enable or disable the plugin. | `true` | +| `pylsp.plugins.jedi_completion.include_params` | `boolean` | Auto-completes methods and classes with tabstops for each parameter. | `true` | +| `pylsp.plugins.jedi_completion.include_class_objects` | `boolean` | Adds class objects as a separate completion item. | `true` | +| `pylsp.plugins.jedi_completion.fuzzy` | `boolean` | Enable fuzzy when requesting autocomplete. | `false` | +| `pylsp.plugins.jedi_completion.eager` | `boolean` | Resolve documentation and detail eagerly. | `false` | +| `pylsp.plugins.jedi_completion.resolve_at_most_labels` | `number` | How many labels (at most) should be resolved? | `25` | +| `pylsp.plugins.jedi_completion.cache_labels_for` | `array` of `string` items | Modules for which the labels should be cached. | `["pandas", "numpy", "tensorflow", "matplotlib"]` | +| `pylsp.plugins.jedi_definition.enabled` | `boolean` | Enable or disable the plugin. | `true` | +| `pylsp.plugins.jedi_definition.follow_imports` | `boolean` | The goto call will follow imports. | `true` | +| `pylsp.plugins.jedi_definition.follow_builtin_imports` | `boolean` | If follow_imports is True will decide if it follow builtin imports. | `true` | +| `pylsp.plugins.jedi_hover.enabled` | `boolean` | Enable or disable the plugin. | `true` | +| `pylsp.plugins.jedi_references.enabled` | `boolean` | Enable or disable the plugin. | `true` | +| `pylsp.plugins.jedi_signature_help.enabled` | `boolean` | Enable or disable the plugin. | `true` | +| `pylsp.plugins.jedi_symbols.enabled` | `boolean` | Enable or disable the plugin. | `true` | +| `pylsp.plugins.jedi_symbols.all_scopes` | `boolean` | If True lists the names of all scopes instead of only the module namespace. | `true` | +| `pylsp.plugins.mccabe.enabled` | `boolean` | Enable or disable the plugin. | `true` | +| `pylsp.plugins.mccabe.threshold` | `number` | The minimum threshold that triggers warnings about cyclomatic complexity. | `15` | +| `pylsp.plugins.preload.enabled` | `boolean` | Enable or disable the plugin. | `true` | +| `pylsp.plugins.preload.modules` | `array` of unique `string` items | List of modules to import on startup | `null` | +| `pylsp.plugins.pycodestyle.enabled` | `boolean` | Enable or disable the plugin. | `true` | +| `pylsp.plugins.pycodestyle.exclude` | `array` of unique `string` items | Exclude files or directories which match these patterns. | `null` | +| `pylsp.plugins.pycodestyle.filename` | `array` of unique `string` items | When parsing directories, only check filenames matching these patterns. | `null` | +| `pylsp.plugins.pycodestyle.select` | `array` of unique `string` items | Select errors and warnings | `null` | +| `pylsp.plugins.pycodestyle.ignore` | `array` of unique `string` items | Ignore errors and warnings | `null` | +| `pylsp.plugins.pycodestyle.hangClosing` | `boolean` | Hang closing bracket instead of matching indentation of opening bracket's line. | `null` | +| `pylsp.plugins.pycodestyle.maxLineLength` | `number` | Set maximum allowed line length. | `null` | +| `pylsp.plugins.pydocstyle.enabled` | `boolean` | Enable or disable the plugin. | `false` | +| `pylsp.plugins.pydocstyle.convention` | `string` | Choose the basic list of checked errors by specifying an existing convention. | `null` | +| `pylsp.plugins.pydocstyle.addIgnore` | `array` of unique `string` items | Ignore errors and warnings in addition to the specified convention. | `null` | +| `pylsp.plugins.pydocstyle.addSelect` | `array` of unique `string` items | Select errors and warnings in addition to the specified convention. | `null` | +| `pylsp.plugins.pydocstyle.ignore` | `array` of unique `string` items | Ignore errors and warnings | `null` | +| `pylsp.plugins.pydocstyle.select` | `array` of unique `string` items | Select errors and warnings | `null` | +| `pylsp.plugins.pydocstyle.match` | `string` | Check only files that exactly match the given regular expression; default is to match files that don't start with 'test_' but end with '.py'. | `"(?!test_).*\\.py"` | +| `pylsp.plugins.pydocstyle.matchDir` | `string` | Search only dirs that exactly match the given regular expression; default is to match dirs which do not begin with a dot. | `"[^\\.].*"` | +| `pylsp.plugins.pyflakes.enabled` | `boolean` | Enable or disable the plugin. | `true` | +| `pylsp.plugins.pylint.enabled` | `boolean` | Enable or disable the plugin. | `false` | +| `pylsp.plugins.pylint.args` | `array` of non-unique `string` items | Arguments to pass to pylint. | `null` | +| `pylsp.plugins.pylint.executable` | `string` | Executable to run pylint with. Enabling this will run pylint on unsaved files via stdin. Can slow down workflow. Only works with python3. | `null` | +| `pylsp.plugins.rope_completion.enabled` | `boolean` | Enable or disable the plugin. | `true` | +| `pylsp.plugins.rope_completion.eager` | `boolean` | Resolve documentation and detail eagerly. | `false` | +| `pylsp.plugins.yapf.enabled` | `boolean` | Enable or disable the plugin. | `true` | +| `pylsp.rope.extensionModules` | `string` | Builtin and c-extension modules that are allowed to be imported and inspected by rope. | `null` | +| `pylsp.rope.ropeFolder` | `array` of unique `string` items | The name of the folder in which rope stores project configurations and data. Pass `null` for not using such a folder at all. | `null` | + +This documentation was generated from `pylsp/config/schema.json`. Please do not edit this file directly. diff --git a/external-deps/python-lsp-server/README.md b/external-deps/python-lsp-server/README.md index 99d5ee98bbd..d0d362e86ce 100644 --- a/external-deps/python-lsp-server/README.md +++ b/external-deps/python-lsp-server/README.md @@ -43,12 +43,13 @@ pip install -U setuptools Installing these plugins will add extra functionality to the language server: -- [pyls-mypy](https://github.com/tomv564/pyls-mypy) Mypy type checking for Python 3 -- [pyls-isort](https://github.com/paradoxxxzero/pyls-isort) Isort import sort code formatting -- [pyls-black](https://github.com/rupert/pyls-black) for code formatting using [Black](https://github.com/ambv/black) -- [pyls-memestra](https://github.com/QuantStack/pyls-memestra) for detecting the use of deprecated APIs +- [pyls-flake8](https://github.com/emanspeaks/pyls-flake8/): Error checking using [flake8](https://flake8.pycqa.org/en/latest/). +- [mypy-ls](https://github.com/Richardk2n/mypy-ls): [MyPy](http://mypy-lang.org/) type checking for Python 3. +- [pyls-isort](https://github.com/paradoxxxzero/pyls-isort): code formatting using [isort](https://github.com/PyCQA/isort) (automatic import sorting). +- [python-lsp-black](https://github.com/python-lsp/python-lsp-black): code formatting using [Black](https://github.com/psf/black). +- [pyls-memestra](https://github.com/QuantStack/pyls-memestra): detecting the use of deprecated APIs. -Please see the above repositories for examples on how to write plugins for the Python Language Server. Please file an issue if you require assistance writing a plugin. +Please see the above repositories for examples on how to write plugins for the Python LSP Server. Please file an issue if you require assistance writing a plugin. ## Configuration @@ -59,12 +60,14 @@ Configuration is loaded from zero or more configuration sources. Currently impl The default configuration source is pycodestyle. Change the `pylsp.configurationSources` setting to `['flake8']` in order to respect flake8 configuration instead. -Overall configuration is computed first from user configuration (in home directory), overridden by configuration passed in by the language client, and then overriden by configuration discovered in the workspace. +Overall configuration is computed first from user configuration (in home directory), overridden by configuration passed in by the language client, and then overridden by configuration discovered in the workspace. To enable pydocstyle for linting docstrings add the following setting in your LSP configuration: `"pylsp.plugins.pydocstyle.enabled": true` -## Language Server Features +All configuration options are described in [`CONFIGURATION.md`](https://github.com/python-lsp/python-lsp-server/blob/develop/CONFIGURATION.md). + +## LSP Server Features * Auto Completion * Code Linting @@ -81,8 +84,14 @@ To enable pydocstyle for linting docstrings add the following setting in your LS To run the test suite: +```sh +pip install '.[test]' && pytest +``` + +After adding configuration options to `schema.json`, refresh the `CONFIGURATION.md` file with + ``` -pip install .[test] && pytest +python scripts/jsonschema2md.py pylsp/config/schema.json CONFIGURATION.md ``` ## License diff --git a/external-deps/python-lsp-server/RELEASE.md b/external-deps/python-lsp-server/RELEASE.md index 35740e9cefe..b716d582907 100644 --- a/external-deps/python-lsp-server/RELEASE.md +++ b/external-deps/python-lsp-server/RELEASE.md @@ -2,16 +2,16 @@ To release a new version of python-lsp-server: 1. git fetch upstream && git checkout upstream/master 2. Close milestone on GitHub 3. git clean -xfdi -4. Update CHANGELOG.md with loghub +4. Update CHANGELOG.md with `loghub python-lsp/python-lsp-server -m vX.X.X` 5. git add -A && git commit -m "Update Changelog" -6. Update release version in ``_version.py`` (set release version, remove 'dev0') +6. Update release version in `_version.py` (set release version, remove 'dev0') 7. git add -A && git commit -m "Release vX.X.X" 8. python setup.py sdist 9. python setup.py bdist_wheel -10. twine check -11. twine upload +10. twine check dist/* +11. twine upload dist/* 12. git tag -a vX.X.X -m "Release vX.X.X" -13. Update development version in ``_version.py`` (add 'dev0' and increment minor) +13. Update development version in `_version.py` (add 'dev0' and increment minor) 14. git add -A && git commit -m "Back to work" -15. git push upstream master +15. git push upstream develop 16. git push upstream --tags diff --git a/external-deps/python-lsp-server/pylsp/__main__.py b/external-deps/python-lsp-server/pylsp/__main__.py index 065cdee9b81..a480823b336 100644 --- a/external-deps/python-lsp-server/pylsp/__main__.py +++ b/external-deps/python-lsp-server/pylsp/__main__.py @@ -5,6 +5,7 @@ import logging import logging.config import sys +import time try: import ujson as json @@ -13,8 +14,10 @@ from .python_lsp import (PythonLSPServer, start_io_lang_server, start_tcp_lang_server) +from ._version import __version__ -LOG_FORMAT = "%(asctime)s UTC - %(levelname)s - %(name)s - %(message)s" +LOG_FORMAT = "%(asctime)s {0} - %(levelname)s - %(name)s - %(message)s".format( + time.localtime().tm_zone) def add_arguments(parser): @@ -55,6 +58,10 @@ def add_arguments(parser): help="Increase verbosity of log output, overrides log config file" ) + parser.add_argument( + '-V', '--version', action='version', version='%(prog)s v' + __version__ + ) + def main(): parser = argparse.ArgumentParser() diff --git a/external-deps/python-lsp-server/pylsp/_utils.py b/external-deps/python-lsp-server/pylsp/_utils.py index dd46701fece..92376f6c43e 100644 --- a/external-deps/python-lsp-server/pylsp/_utils.py +++ b/external-deps/python-lsp-server/pylsp/_utils.py @@ -125,6 +125,8 @@ def _merge_dicts_(a, b): if key in a and key in b: if isinstance(a[key], dict) and isinstance(b[key], dict): yield (key, dict(_merge_dicts_(a[key], b[key]))) + elif isinstance(a[key], list) and isinstance(b[key], list): + yield (key, list(set(a[key] + b[key]))) elif b[key] is not None: yield (key, b[key]) else: diff --git a/external-deps/python-lsp-server/pylsp/_version.py b/external-deps/python-lsp-server/pylsp/_version.py index 64e0c9b62cc..02f30cef417 100644 --- a/external-deps/python-lsp-server/pylsp/_version.py +++ b/external-deps/python-lsp-server/pylsp/_version.py @@ -4,5 +4,5 @@ """PyLSP versioning information.""" -VERSION_INFO = (1, 1, 0, 'dev0') +VERSION_INFO = (1, 2, 0, 'dev0') __version__ = '.'.join(map(str, VERSION_INFO)) diff --git a/external-deps/python-lsp-server/pylsp/config/flake8_conf.py b/external-deps/python-lsp-server/pylsp/config/flake8_conf.py index e2c90099c48..eed9a31cc10 100644 --- a/external-deps/python-lsp-server/pylsp/config/flake8_conf.py +++ b/external-deps/python-lsp-server/pylsp/config/flake8_conf.py @@ -28,6 +28,7 @@ ('ignore', 'plugins.flake8.ignore', list), ('max-line-length', 'plugins.flake8.maxLineLength', int), ('select', 'plugins.flake8.select', list), + ('per-file-ignores', 'plugins.flake8.perFileIgnores', list), ] @@ -48,3 +49,9 @@ def project_config(self, document_path): files = find_parents(self.root_path, document_path, PROJECT_CONFIGS) config = self.read_config_from_files(files) return self.parse_config(config, CONFIG_KEY, OPTIONS) + + @classmethod + def _parse_list_opt(cls, string): + if string.startswith("\n"): + return [s.strip().rstrip(",") for s in string.split("\n") if s.strip()] + return [s.strip() for s in string.split(",") if s.strip()] diff --git a/external-deps/python-lsp-server/pylsp/config/schema.json b/external-deps/python-lsp-server/pylsp/config/schema.json new file mode 100644 index 00000000000..b04676531e0 --- /dev/null +++ b/external-deps/python-lsp-server/pylsp/config/schema.json @@ -0,0 +1,299 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Python Language Server Configuration", + "description": "This server can be configured using `workspace/didChangeConfiguration` method. Each configuration option is described below:", + "type": "object", + "properties": { + "pylsp.configurationSources": { + "type": "array", + "default": ["pycodestyle"], + "description": "List of configuration sources to use.", + "items": { + "type": "string", + "enum": ["pycodestyle", "pyflakes"] + }, + "uniqueItems": true + }, + "pylsp.plugins.jedi.extra_paths": { + "type": "array", + "default": [], + "description": "Define extra paths for jedi.Script." + }, + "pylsp.plugins.jedi.env_vars": { + "type": "object", + "default": null, + "description": "Define environment variables for jedi.Script and Jedi.names." + }, + "pylsp.plugins.jedi.environment": { + "type": "string", + "default": null, + "description": "Define environment for jedi.Script and Jedi.names." + }, + "pylsp.plugins.jedi_completion.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable the plugin." + }, + "pylsp.plugins.jedi_completion.include_params": { + "type": "boolean", + "default": true, + "description": "Auto-completes methods and classes with tabstops for each parameter." + }, + "pylsp.plugins.jedi_completion.include_class_objects": { + "type": "boolean", + "default": true, + "description": "Adds class objects as a separate completion item." + }, + "pylsp.plugins.jedi_completion.fuzzy": { + "type": "boolean", + "default": false, + "description": "Enable fuzzy when requesting autocomplete." + }, + "pylsp.plugins.jedi_completion.eager": { + "type": "boolean", + "default": false, + "description": "Resolve documentation and detail eagerly." + }, + "pylsp.plugins.jedi_completion.resolve_at_most_labels": { + "type": "number", + "default": 25, + "description": "How many labels (at most) should be resolved?" + }, + "pylsp.plugins.jedi_completion.cache_labels_for": { + "type": "array", + "items": { + "type": "string" + }, + "default": ["pandas", "numpy", "tensorflow", "matplotlib"], + "description": "Modules for which the labels should be cached." + }, + "pylsp.plugins.jedi_definition.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable the plugin." + }, + "pylsp.plugins.jedi_definition.follow_imports": { + "type": "boolean", + "default": true, + "description": "The goto call will follow imports." + }, + "pylsp.plugins.jedi_definition.follow_builtin_imports": { + "type": "boolean", + "default": true, + "description": "If follow_imports is True will decide if it follow builtin imports." + }, + "pylsp.plugins.jedi_hover.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable the plugin." + }, + "pylsp.plugins.jedi_references.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable the plugin." + }, + "pylsp.plugins.jedi_signature_help.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable the plugin." + }, + "pylsp.plugins.jedi_symbols.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable the plugin." + }, + "pylsp.plugins.jedi_symbols.all_scopes": { + "type": "boolean", + "default": true, + "description": "If True lists the names of all scopes instead of only the module namespace." + }, + "pylsp.plugins.mccabe.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable the plugin." + }, + "pylsp.plugins.mccabe.threshold": { + "type": "number", + "default": 15, + "description": "The minimum threshold that triggers warnings about cyclomatic complexity." + }, + "pylsp.plugins.preload.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable the plugin." + }, + "pylsp.plugins.preload.modules": { + "type": "array", + "default": null, + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "List of modules to import on startup" + }, + "pylsp.plugins.pycodestyle.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable the plugin." + }, + "pylsp.plugins.pycodestyle.exclude": { + "type": "array", + "default": null, + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "Exclude files or directories which match these patterns." + }, + "pylsp.plugins.pycodestyle.filename": { + "type": "array", + "default": null, + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "When parsing directories, only check filenames matching these patterns." + }, + "pylsp.plugins.pycodestyle.select": { + "type": "array", + "default": null, + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "Select errors and warnings" + }, + "pylsp.plugins.pycodestyle.ignore": { + "type": "array", + "default": null, + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "Ignore errors and warnings" + }, + "pylsp.plugins.pycodestyle.hangClosing": { + "type": "boolean", + "default": null, + "description": "Hang closing bracket instead of matching indentation of opening bracket's line." + }, + "pylsp.plugins.pycodestyle.maxLineLength": { + "type": "number", + "default": null, + "description": "Set maximum allowed line length." + }, + "pylsp.plugins.pydocstyle.enabled": { + "type": "boolean", + "default": false, + "description": "Enable or disable the plugin." + }, + "pylsp.plugins.pydocstyle.convention": { + "type": "string", + "default": null, + "enum": [ + "pep257", + "numpy" + ], + "description": "Choose the basic list of checked errors by specifying an existing convention." + }, + "pylsp.plugins.pydocstyle.addIgnore": { + "type": "array", + "default": null, + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "Ignore errors and warnings in addition to the specified convention." + }, + "pylsp.plugins.pydocstyle.addSelect": { + "type": "array", + "default": null, + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "Select errors and warnings in addition to the specified convention." + }, + "pylsp.plugins.pydocstyle.ignore": { + "type": "array", + "default": null, + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "Ignore errors and warnings" + }, + "pylsp.plugins.pydocstyle.select": { + "type": "array", + "default": null, + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "Select errors and warnings" + }, + "pylsp.plugins.pydocstyle.match": { + "type": "string", + "default": "(?!test_).*\\.py", + "description": "Check only files that exactly match the given regular expression; default is to match files that don't start with 'test_' but end with '.py'." + }, + "pylsp.plugins.pydocstyle.matchDir": { + "type": "string", + "default": "[^\\.].*", + "description": "Search only dirs that exactly match the given regular expression; default is to match dirs which do not begin with a dot." + }, + "pylsp.plugins.pyflakes.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable the plugin." + }, + "pylsp.plugins.pylint.enabled": { + "type": "boolean", + "default": false, + "description": "Enable or disable the plugin." + }, + "pylsp.plugins.pylint.args": { + "type": "array", + "default": null, + "items": { + "type": "string" + }, + "uniqueItems": false, + "description": "Arguments to pass to pylint." + }, + "pylsp.plugins.pylint.executable": { + "type": "string", + "default": null, + "description": "Executable to run pylint with. Enabling this will run pylint on unsaved files via stdin. Can slow down workflow. Only works with python3." + }, + "pylsp.plugins.rope_completion.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable the plugin." + }, + "pylsp.plugins.rope_completion.eager": { + "type": "boolean", + "default": false, + "description": "Resolve documentation and detail eagerly." + }, + "pylsp.plugins.yapf.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable the plugin." + }, + "pylsp.rope.extensionModules": { + "type": "string", + "default": null, + "description": "Builtin and c-extension modules that are allowed to be imported and inspected by rope." + }, + "pylsp.rope.ropeFolder": { + "type": "array", + "default": null, + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "The name of the folder in which rope stores project configurations and data. Pass `null` for not using such a folder at all." + } + } +} diff --git a/external-deps/python-lsp-server/pylsp/config/source.py b/external-deps/python-lsp-server/pylsp/config/source.py index 87fe7156aff..6a21a84c158 100644 --- a/external-deps/python-lsp-server/pylsp/config/source.py +++ b/external-deps/python-lsp-server/pylsp/config/source.py @@ -27,8 +27,8 @@ def project_config(self, document_path): """Return project-level (i.e. workspace directory) configuration.""" raise NotImplementedError() - @staticmethod - def read_config_from_files(files): + @classmethod + def read_config_from_files(cls, files): config = configparser.RawConfigParser() for filename in files: if os.path.exists(filename) and not os.path.isdir(filename): @@ -36,53 +36,53 @@ def read_config_from_files(files): return config - @staticmethod - def parse_config(config, key, options): + @classmethod + def parse_config(cls, config, key, options): """Parse the config with the given options.""" conf = {} for source, destination, opt_type in options: - opt_value = _get_opt(config, key, source, opt_type) + opt_value = cls._get_opt(config, key, source, opt_type) if opt_value is not None: - _set_opt(conf, destination, opt_value) + cls._set_opt(conf, destination, opt_value) return conf + @classmethod + def _get_opt(cls, config, key, option, opt_type): + """Get an option from a configparser with the given type.""" + for opt_key in [option, option.replace('-', '_')]: + if not config.has_option(key, opt_key): + continue -def _get_opt(config, key, option, opt_type): - """Get an option from a configparser with the given type.""" - for opt_key in [option, option.replace('-', '_')]: - if not config.has_option(key, opt_key): - continue + if opt_type == bool: + return config.getboolean(key, opt_key) - if opt_type == bool: - return config.getboolean(key, opt_key) + if opt_type == int: + return config.getint(key, opt_key) - if opt_type == int: - return config.getint(key, opt_key) + if opt_type == str: + return config.get(key, opt_key) - if opt_type == str: - return config.get(key, opt_key) + if opt_type == list: + return cls._parse_list_opt(config.get(key, opt_key)) - if opt_type == list: - return _parse_list_opt(config.get(key, opt_key)) + raise ValueError("Unknown option type: %s" % opt_type) - raise ValueError("Unknown option type: %s" % opt_type) + @classmethod + def _parse_list_opt(cls, string): + return [s.strip() for s in string.split(",") if s.strip()] + @classmethod + def _set_opt(cls, config_dict, path, value): + """Set the value in the dictionary at the given path if the value is not None.""" + if value is None: + return -def _parse_list_opt(string): - return [s.strip() for s in string.split(",") if s.strip()] + if '.' not in path: + config_dict[path] = value + return + key, rest = path.split(".", 1) + if key not in config_dict: + config_dict[key] = {} -def _set_opt(config_dict, path, value): - """Set the value in the dictionary at the given path if the value is not None.""" - if value is None: - return - - if '.' not in path: - config_dict[path] = value - return - - key, rest = path.split(".", 1) - if key not in config_dict: - config_dict[key] = {} - - _set_opt(config_dict[key], rest, value) + cls._set_opt(config_dict[key], rest, value) diff --git a/external-deps/python-lsp-server/pylsp/hookspecs.py b/external-deps/python-lsp-server/pylsp/hookspecs.py index ff2925ac55b..736cf931826 100644 --- a/external-deps/python-lsp-server/pylsp/hookspecs.py +++ b/external-deps/python-lsp-server/pylsp/hookspecs.py @@ -29,6 +29,11 @@ def pylsp_completions(config, workspace, document, position): pass +@hookspec(firstresult=True) +def pylsp_completion_item_resolve(config, workspace, document, completion_item): + pass + + @hookspec def pylsp_definitions(config, workspace, document, position): pass diff --git a/external-deps/python-lsp-server/pylsp/plugins/flake8_lint.py b/external-deps/python-lsp-server/pylsp/plugins/flake8_lint.py index 03504ef4ad1..7ac8c62283b 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/flake8_lint.py +++ b/external-deps/python-lsp-server/pylsp/plugins/flake8_lint.py @@ -5,7 +5,9 @@ import logging import os.path import re -from subprocess import Popen, PIPE +from pathlib import PurePath +from subprocess import PIPE, Popen + from pylsp import hookimpl, lsp log = logging.getLogger(__name__) @@ -24,12 +26,21 @@ def pylsp_lint(workspace, document): settings = config.plugin_settings('flake8', document_path=document.path) log.debug("Got flake8 settings: %s", settings) + ignores = settings.get("ignore", []) + per_file_ignores = settings.get("perFileIgnores") + + if per_file_ignores: + for path in per_file_ignores: + file_pat, errors = path.split(":") + if PurePath(document.path).match(file_pat): + ignores.extend(errors.split(",")) + opts = { 'config': settings.get('config'), 'exclude': settings.get('exclude'), 'filename': settings.get('filename'), 'hang-closing': settings.get('hangClosing'), - 'ignore': settings.get('ignore'), + 'ignore': ignores or None, 'max-line-length': settings.get('maxLineLength'), 'select': settings.get('select'), } diff --git a/external-deps/python-lsp-server/pylsp/plugins/jedi_completion.py b/external-deps/python-lsp-server/pylsp/plugins/jedi_completion.py index 5b07f56b45a..a07e5254f0e 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/jedi_completion.py +++ b/external-deps/python-lsp-server/pylsp/plugins/jedi_completion.py @@ -3,7 +3,10 @@ import logging import os.path as osp +from collections import defaultdict +from time import time +from jedi.api.classes import Completion import parso from pylsp import _utils, hookimpl, lsp @@ -37,10 +40,12 @@ @hookimpl def pylsp_completions(config, document, position): """Get formatted completions for current code position""" + # pylint: disable=too-many-locals settings = config.plugin_settings('jedi_completion', document_path=document.path) + resolve_eagerly = settings.get('eager', False) code_position = _utils.position_to_jedi_linecolumn(document, position) - code_position["fuzzy"] = settings.get("fuzzy", False) + code_position['fuzzy'] = settings.get('fuzzy', False) completions = document.jedi_script(use_document_path=True).complete(**code_position) if not completions: @@ -52,25 +57,63 @@ def pylsp_completions(config, document, position): should_include_params = settings.get('include_params') should_include_class_objects = settings.get('include_class_objects', True) + max_labels_resolve = settings.get('resolve_at_most_labels', 25) + modules_to_cache_labels_for = settings.get('cache_labels_for', None) + if modules_to_cache_labels_for is not None: + LABEL_RESOLVER.cached_modules = modules_to_cache_labels_for + include_params = snippet_support and should_include_params and use_snippets(document, position) include_class_objects = snippet_support and should_include_class_objects and use_snippets(document, position) ready_completions = [ - _format_completion(c, include_params) - for c in completions + _format_completion( + c, + include_params, + resolve=resolve_eagerly, + resolve_label=(i < max_labels_resolve) + ) + for i, c in enumerate(completions) ] + # TODO split up once other improvements are merged if include_class_objects: - for c in completions: + for i, c in enumerate(completions): if c.type == 'class': - completion_dict = _format_completion(c, False) + completion_dict = _format_completion( + c, + False, + resolve=resolve_eagerly, + resolve_label=(i < max_labels_resolve) + ) completion_dict['kind'] = lsp.CompletionItemKind.TypeParameter completion_dict['label'] += ' object' ready_completions.append(completion_dict) + for completion_dict in ready_completions: + completion_dict['data'] = { + 'doc_uri': document.uri + } + + # most recently retrieved completion items, used for resolution + document.shared_data['LAST_JEDI_COMPLETIONS'] = { + # label is the only required property; here it is assumed to be unique + completion['label']: (completion, data) + for completion, data in zip(ready_completions, completions) + } + return ready_completions or None +@hookimpl +def pylsp_completion_item_resolve(completion_item, document): + """Resolve formatted completion for given non-resolved completion""" + shared_data = document.shared_data['LAST_JEDI_COMPLETIONS'].get(completion_item['label']) + if shared_data: + completion, data = shared_data + return _resolve_completion(completion, data) + return completion_item + + def is_exception_class(name): """ Determine if a class name is an instance of an Exception. @@ -114,31 +157,41 @@ def use_snippets(document, position): break if '(' in act_lines[-1].strip(): last_character = ')' - code = '\n'.join(act_lines).split(';')[-1].strip() + last_character + code = '\n'.join(act_lines).rsplit(';', maxsplit=1)[-1].strip() + last_character tokens = parso.parse(code) expr_type = tokens.children[0].type return (expr_type not in _IMPORTS and not (expr_type in _ERRORS and 'import' in code)) -def _format_completion(d, include_params=True): +def _resolve_completion(completion, d): + completion['detail'] = _detail(d) + completion['documentation'] = _utils.format_docstring(d.docstring()) + return completion + + +def _format_completion(d, include_params=True, resolve=False, resolve_label=False): completion = { - 'label': _label(d), + 'label': _label(d, resolve_label), 'kind': _TYPE_MAP.get(d.type), - 'detail': _detail(d), - 'documentation': _utils.format_docstring(d.docstring()), 'sortText': _sort_text(d), 'insertText': d.name } + if resolve: + completion = _resolve_completion(completion, d) + if d.type == 'path': path = osp.normpath(d.name) path = path.replace('\\', '\\\\') path = path.replace('/', '\\/') completion['insertText'] = path - sig = d.get_signatures() - if (include_params and sig and not is_exception_class(d.name)): + if include_params and not is_exception_class(d.name): + sig = d.get_signatures() + if not sig: + return completion + positional_args = [param for param in sig[0].params if '=' not in param.description and param.name not in {'/', '*'}] @@ -162,12 +215,12 @@ def _format_completion(d, include_params=True): return completion -def _label(definition): - sig = definition.get_signatures() - if definition.type in ('function', 'method') and sig: - params = ', '.join(param.name for param in sig[0].params) - return '{}({})'.format(definition.name, params) - +def _label(definition, resolve=False): + if not resolve: + return definition.name + sig = LABEL_RESOLVER.get_or_create(definition) + if sig: + return sig return definition.name @@ -186,3 +239,86 @@ def _sort_text(definition): # If its 'hidden', put it next last prefix = 'z{}' if definition.name.startswith('_') else 'a{}' return prefix.format(definition.name) + + +class LabelResolver: + + def __init__(self, format_label_callback, time_to_live=60 * 30): + self.format_label = format_label_callback + self._cache = {} + self._time_to_live = time_to_live + self._cache_ttl = defaultdict(set) + self._clear_every = 2 + # see https://github.com/davidhalter/jedi/blob/master/jedi/inference/helpers.py#L194-L202 + self._cached_modules = {'pandas', 'numpy', 'tensorflow', 'matplotlib'} + + @property + def cached_modules(self): + return self._cached_modules + + @cached_modules.setter + def cached_modules(self, new_value): + self._cached_modules = set(new_value) + + def clear_outdated(self): + now = self.time_key() + to_clear = [ + timestamp + for timestamp in self._cache_ttl + if timestamp < now + ] + for time_key in to_clear: + for key in self._cache_ttl[time_key]: + del self._cache[key] + del self._cache_ttl[time_key] + + def time_key(self): + return int(time() / self._time_to_live) + + def get_or_create(self, completion: Completion): + if not completion.full_name: + use_cache = False + else: + module_parts = completion.full_name.split('.') + use_cache = module_parts and module_parts[0] in self._cached_modules + + if use_cache: + key = self._create_completion_id(completion) + if key not in self._cache: + if self.time_key() % self._clear_every == 0: + self.clear_outdated() + + self._cache[key] = self.resolve_label(completion) + self._cache_ttl[self.time_key()].add(key) + return self._cache[key] + + return self.resolve_label(completion) + + def _create_completion_id(self, completion: Completion): + return ( + completion.full_name, completion.module_path, + completion.line, completion.column, + self.time_key() + ) + + def resolve_label(self, completion): + try: + sig = completion.get_signatures() + return self.format_label(completion, sig) + except Exception as e: # pylint: disable=broad-except + log.warning( + 'Something went wrong when resolving label for {completion}: {e}', + completion=completion, e=e + ) + return '' + + +def format_label(completion, sig): + if sig and completion.type in ('function', 'method'): + params = ', '.join(param.name for param in sig[0].params) + label = '{}({})'.format(completion.name, params) + return label + return completion.name + + +LABEL_RESOLVER = LabelResolver(format_label) diff --git a/external-deps/python-lsp-server/pylsp/plugins/rope_completion.py b/external-deps/python-lsp-server/pylsp/plugins/rope_completion.py index c1e162a277c..b73d8412196 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/rope_completion.py +++ b/external-deps/python-lsp-server/pylsp/plugins/rope_completion.py @@ -13,11 +13,26 @@ @hookimpl def pylsp_settings(): # Default rope_completion to disabled - return {'plugins': {'rope_completion': {'enabled': False}}} + return {'plugins': {'rope_completion': {'enabled': False, 'eager': False}}} + + +def _resolve_completion(completion, data): + try: + doc = data.get_doc() + except AttributeError: + doc = "" + completion['detail'] = '{0} {1}'.format(data.scope or "", data.name) + completion['documentation'] = doc + return completion @hookimpl def pylsp_completions(config, workspace, document, position): + # pylint: disable=too-many-locals + + settings = config.plugin_settings('rope_completion', document_path=document.path) + resolve_eagerly = settings.get('eager', False) + # Rope is a bit rubbish at completing module imports, so we'll return None word = document.word_at_position({ # The -1 should really be trying to look at the previous word, but that might be quite expensive @@ -41,22 +56,37 @@ def pylsp_completions(config, workspace, document, position): definitions = sorted_proposals(definitions) new_definitions = [] for d in definitions: - try: - doc = d.get_doc() - except AttributeError: - doc = None - new_definitions.append({ + item = { 'label': d.name, 'kind': _kind(d), - 'detail': '{0} {1}'.format(d.scope or "", d.name), - 'documentation': doc or "", - 'sortText': _sort_text(d) - }) + 'sortText': _sort_text(d), + 'data': { + 'doc_uri': document.uri + } + } + if resolve_eagerly: + item = _resolve_completion(item, d) + new_definitions.append(item) + + # most recently retrieved completion items, used for resolution + document.shared_data['LAST_ROPE_COMPLETIONS'] = { + # label is the only required property; here it is assumed to be unique + completion['label']: (completion, data) + for completion, data in zip(new_definitions, definitions) + } + definitions = new_definitions return definitions or None +@hookimpl +def pylsp_completion_item_resolve(completion_item, document): + """Resolve formatted completion for given non-resolved completion""" + completion, data = document.shared_data['LAST_ROPE_COMPLETIONS'].get(completion_item['label']) + return _resolve_completion(completion, data) + + def _sort_text(definition): """ Ensure builtins appear at the bottom. Description is of format : . @@ -72,7 +102,7 @@ def _sort_text(definition): def _kind(d): - """ Return the VSCode type """ + """ Return the LSP type """ MAP = { 'none': lsp.CompletionItemKind.Value, 'type': lsp.CompletionItemKind.Class, diff --git a/external-deps/python-lsp-server/pylsp/python_lsp.py b/external-deps/python-lsp-server/pylsp/python_lsp.py index d4c98306010..ec099234c50 100644 --- a/external-deps/python-lsp-server/pylsp/python_lsp.py +++ b/external-deps/python-lsp-server/pylsp/python_lsp.py @@ -14,6 +14,7 @@ from . import lsp, _utils, uris from .config import config from .workspace import Workspace +from ._version import __version__ log = logging.getLogger(__name__) @@ -161,8 +162,8 @@ def capabilities(self): 'resolveProvider': False, # We may need to make this configurable }, 'completionProvider': { - 'resolveProvider': False, # We know everything ahead of time - 'triggerCharacters': ['.'] + 'resolveProvider': True, # We could know everything ahead of time, but this takes time to transfer + 'triggerCharacters': ['.'], }, 'documentFormattingProvider': True, 'documentHighlightProvider': True, @@ -198,7 +199,8 @@ def capabilities(self): log.info('Server capabilities: %s', server_capabilities) return server_capabilities - def m_initialize(self, processId=None, rootUri=None, rootPath=None, initializationOptions=None, **_kwargs): + def m_initialize(self, processId=None, rootUri=None, rootPath=None, + initializationOptions=None, workspaceFolders=None, **_kwargs): log.debug('Language server initialized with %s %s %s %s', processId, rootUri, rootPath, initializationOptions) if rootUri is None: rootUri = uris.from_fs_path(rootPath) if rootPath is not None else '' @@ -209,6 +211,19 @@ def m_initialize(self, processId=None, rootUri=None, rootPath=None, initializati processId, _kwargs.get('capabilities', {})) self.workspace = Workspace(rootUri, self._endpoint, self.config) self.workspaces[rootUri] = self.workspace + if workspaceFolders: + for folder in workspaceFolders: + uri = folder['uri'] + if uri == rootUri: + # Already created + continue + workspace_config = config.Config( + uri, self.config._init_opts, + self.config._process_id, self.config._capabilities) + workspace_config.update(self.config._settings) + self.workspaces[uri] = Workspace( + uri, self._endpoint, workspace_config) + self._dispatchers = self._hook('pylsp_dispatchers') self._hook('pylsp_initialize') @@ -225,7 +240,13 @@ def watch_parent_process(pid): self.watching_thread.daemon = True self.watching_thread.start() # Get our capabilities - return {'capabilities': self.capabilities()} + return { + 'capabilities': self.capabilities(), + 'serverInfo': { + 'name': 'pylsp', + 'version': __version__, + }, + } def m_initialized(self, **_kwargs): self._hook('pylsp_initialized') @@ -243,6 +264,10 @@ def completions(self, doc_uri, position): 'items': flatten(completions) } + def completion_item_resolve(self, completion_item): + doc_uri = completion_item.get('data', {}).get('doc_uri', None) + return self._hook('pylsp_completion_item_resolve', doc_uri, completion_item=completion_item) + def definitions(self, doc_uri, position): return flatten(self._hook('pylsp_definitions', doc_uri, position=position)) @@ -289,6 +314,9 @@ def signature_help(self, doc_uri, position): def folding(self, doc_uri): return flatten(self._hook('pylsp_folding_range', doc_uri)) + def m_completion_item__resolve(self, **completionItem): + return self.completion_item_resolve(completionItem) + def m_text_document__did_close(self, textDocument=None, **_kwargs): workspace = self._match_uri_to_workspace(textDocument['uri']) workspace.rm_document(textDocument['uri']) @@ -356,8 +384,7 @@ def m_text_document__signature_help(self, textDocument=None, position=None, **_k def m_workspace__did_change_configuration(self, settings=None): self.config.update((settings or {}).get('pylsp', {})) - for workspace_uri in self.workspaces: - workspace = self.workspaces[workspace_uri] + for workspace in self.workspaces.values(): workspace.update_config(settings) for doc_uri in workspace.documents: self.lint(doc_uri, is_saved=False) @@ -426,8 +453,7 @@ def m_workspace__did_change_watched_files(self, changes=None, **_kwargs): # Only externally changed python files and lint configs may result in changed diagnostics. return - for workspace_uri in self.workspaces: - workspace = self.workspaces[workspace_uri] + for workspace in self.workspaces.values(): for doc_uri in workspace.documents: # Changes in doc_uri are already handled by m_text_document__did_save if doc_uri not in changed_py_files: diff --git a/external-deps/python-lsp-server/pylsp/workspace.py b/external-deps/python-lsp-server/pylsp/workspace.py index 20720c9b36e..a063cabe6e2 100644 --- a/external-deps/python-lsp-server/pylsp/workspace.py +++ b/external-deps/python-lsp-server/pylsp/workspace.py @@ -138,6 +138,7 @@ def __init__(self, uri, workspace, source=None, version=None, local=True, extra_ self.path = uris.to_fs_path(uri) self.dot_path = _utils.path_to_dot_name(self.path) self.filename = os.path.basename(self.path) + self.shared_data = {} self._config = workspace._config self._workspace = workspace diff --git a/external-deps/python-lsp-server/scripts/jsonschema2md.py b/external-deps/python-lsp-server/scripts/jsonschema2md.py new file mode 100644 index 00000000000..b10de88690f --- /dev/null +++ b/external-deps/python-lsp-server/scripts/jsonschema2md.py @@ -0,0 +1,81 @@ +import json +import sys +from argparse import ArgumentParser, FileType + + +def describe_array(prop: dict) -> str: + extra = "" + if "items" in prop: + unique_qualifier = "" + if "uniqueItems" in prop: + unique_qualifier = "unique" if prop["uniqueItems"] else "non-unique" + item_type = describe_type(prop["items"]) + extra += f" of {unique_qualifier} {item_type} items" + return extra + + +def describe_number(prop: dict) -> str: + extra = [] + if "minimum" in prop: + extra.append(f">= {prop['minimum']}") + if "maximum" in prop: + extra.append(f"<= {prop['maximum']}") + return ",".join(extra) + + +EXTRA_DESCRIPTORS = { + "array": describe_array, + "number": describe_number, +} + + +def describe_type(prop: dict) -> str: + prop_type = prop["type"] + label = f"`{prop_type}`" + if prop_type in EXTRA_DESCRIPTORS: + label += " " + EXTRA_DESCRIPTORS[prop_type](prop) + if "enum" in prop: + allowed_values = [f"`{value}`" for value in prop["enum"]] + label += "one of: " + ", ".join(allowed_values) + return label + + +def convert_schema(schema: dict, source: str = None) -> str: + lines = [ + f"# {schema['title']}", + schema["description"], + "", + "| **Configuration Key** | **Type** | **Description** | **Default** ", + "|----|----|----|----|", + ] + for key, prop in schema["properties"].items(): + description = prop.get("description", "") + default = json.dumps(prop.get("default", "")) + lines.append( + f"| `{key}` | {describe_type(prop)} | {description} | `{default}` |" + ) + + if source: + lines.append( + f"\nThis documentation was generated from `{source}`." + " Please do not edit this file directly." + ) + + # ensure empty line at the end + lines.append("") + + return "\n".join(lines) + + +def main(argv): + parser = ArgumentParser() + parser.add_argument("schema", type=FileType()) + parser.add_argument("markdown", type=FileType("w+"), default=sys.stdout) + arguments = parser.parse_args(argv[1:]) + schema = json.loads(arguments.schema.read()) + markdown = convert_schema(schema, source=arguments.schema.name) + arguments.markdown.write(markdown) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/external-deps/python-lsp-server/test/fixtures.py b/external-deps/python-lsp-server/test/fixtures.py index c50c1c10643..cce5b328f44 100644 --- a/external-deps/python-lsp-server/test/fixtures.py +++ b/external-deps/python-lsp-server/test/fixtures.py @@ -34,6 +34,34 @@ def pylsp(tmpdir): return ls +@pytest.fixture +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) + + @pytest.fixture def workspace(tmpdir): """Return a workspace.""" diff --git a/external-deps/python-lsp-server/test/plugins/test_completion.py b/external-deps/python-lsp-server/test/plugins/test_completion.py index df197248ac1..1afee4ac089 100644 --- a/external-deps/python-lsp-server/test/plugins/test_completion.py +++ b/external-deps/python-lsp-server/test/plugins/test_completion.py @@ -1,6 +1,7 @@ # Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. +import math import os import sys @@ -12,6 +13,7 @@ from pylsp import uris, lsp from pylsp.workspace import Document from pylsp.plugins.jedi_completion import pylsp_completions as pylsp_jedi_completions +from pylsp.plugins.jedi_completion import pylsp_completion_item_resolve as pylsp_jedi_completion_item_resolve from pylsp.plugins.rope_completion import pylsp_completions as pylsp_rope_completions from pylsp._utils import JEDI_VERSION @@ -44,6 +46,10 @@ def everyone(self, a, b, c=None, d=2): print Hello().world print Hello().every + +def documented_hello(): + \"\"\"Sends a polite greeting\"\"\" + pass """ @@ -139,6 +145,27 @@ def test_jedi_completion(config, workspace): pylsp_jedi_completions(config, doc, {'line': 1, 'character': 1000}) +def test_jedi_completion_item_resolve(config, workspace): + # Over the blank line + com_position = {'line': 8, 'character': 0} + doc = Document(DOC_URI, workspace, DOC) + config.update({'plugins': {'jedi_completion': {'resolve_at_most_labels': math.inf}}}) + completions = pylsp_jedi_completions(config, doc, com_position) + + items = {c['label']: c for c in completions} + + documented_hello_item = items['documented_hello()'] + + assert 'documentation' not in documented_hello_item + assert 'detail' not in documented_hello_item + + resolved_documented_hello = pylsp_jedi_completion_item_resolve( + completion_item=documented_hello_item, + document=doc + ) + assert 'Sends a polite greeting' in resolved_documented_hello['documentation'] + + def test_jedi_completion_with_fuzzy_enabled(config, workspace): # Over 'i' in os.path.isabs(...) config.update({'plugins': {'jedi_completion': {'fuzzy': True}}}) @@ -154,6 +181,24 @@ def test_jedi_completion_with_fuzzy_enabled(config, workspace): pylsp_jedi_completions(config, doc, {'line': 1, 'character': 1000}) +def test_jedi_completion_resolve_at_most(config, workspace): + # Over 'i' in os.path.isabs(...) + com_position = {'line': 1, 'character': 15} + doc = Document(DOC_URI, workspace, DOC) + + # Do not resolve any labels + config.update({'plugins': {'jedi_completion': {'resolve_at_most_labels': 0}}}) + items = pylsp_jedi_completions(config, doc, com_position) + labels = {i['label'] for i in items} + assert 'isabs' in labels + + # Resolve all items + config.update({'plugins': {'jedi_completion': {'resolve_at_most_labels': math.inf}}}) + items = pylsp_jedi_completions(config, doc, com_position) + labels = {i['label'] for i in items} + assert 'isabs(path)' in labels + + def test_rope_completion(config, workspace): # Over 'i' in os.path.isabs(...) com_position = {'line': 1, 'character': 15} @@ -169,6 +214,7 @@ def test_jedi_completion_ordering(config, workspace): # Over the blank line com_position = {'line': 8, 'character': 0} doc = Document(DOC_URI, workspace, DOC) + config.update({'plugins': {'jedi_completion': {'resolve_at_most_labels': math.inf}}}) completions = pylsp_jedi_completions(config, doc, com_position) items = {c['label']: c['sortText'] for c in completions} @@ -410,7 +456,9 @@ def test_jedi_completion_environment(workspace): # After 'import logh' with new environment completions = pylsp_jedi_completions(doc._config, doc, com_position) assert completions[0]['label'] == 'loghub' - assert 'changelog generator' in completions[0]['documentation'].lower() + + resolved = pylsp_jedi_completion_item_resolve(completions[0], doc) + assert 'changelog generator' in resolved['documentation'].lower() def test_document_path_completions(tmpdir, workspace_other_root_path): diff --git a/external-deps/python-lsp-server/test/plugins/test_flake8_lint.py b/external-deps/python-lsp-server/test/plugins/test_flake8_lint.py index 4faf0ddabea..046127c9fc9 100644 --- a/external-deps/python-lsp-server/test/plugins/test_flake8_lint.py +++ b/external-deps/python-lsp-server/test/plugins/test_flake8_lint.py @@ -85,3 +85,77 @@ def test_flake8_executable_param(workspace): (call_args,) = popen_mock.call_args[0] assert flake8_executable in call_args + + +def get_flake8_cfg_settings(workspace, config_str): + """Write a ``setup.cfg``, load it in the workspace, and return the flake8 settings. + + This function creates a ``setup.cfg``; you'll have to delete it yourself. + """ + + with open(os.path.join(workspace.root_path, "setup.cfg"), "w+") as f: + f.write(config_str) + + workspace.update_config({"pylsp": {"configurationSources": ["flake8"]}}) + + return workspace._config.plugin_settings("flake8") + + +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_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")) diff --git a/external-deps/python-lsp-server/test/test_utils.py b/external-deps/python-lsp-server/test/test_utils.py index 0ce6d9ddc96..4b41155bb4a 100644 --- a/external-deps/python-lsp-server/test/test_utils.py +++ b/external-deps/python-lsp-server/test/test_utils.py @@ -2,14 +2,14 @@ # Copyright 2021- Python Language Server Contributors. import time +from unittest import mock -import unittest.mock as mock from flaky import flaky from pylsp import _utils -@flaky +@flaky(max_runs=6, min_passes=1) def test_debounce(): interval = 0.1 obj = mock.Mock() @@ -33,7 +33,7 @@ def call_m(): assert len(obj.mock_calls) == 2 -@flaky +@flaky(max_runs=6, min_passes=1) def test_debounce_keyed_by(): interval = 0.1 obj = mock.Mock() diff --git a/external-deps/python-lsp-server/test/test_workspace.py b/external-deps/python-lsp-server/test/test_workspace.py index e8851e05854..a008e7eb351 100644 --- a/external-deps/python-lsp-server/test/test_workspace.py +++ b/external-deps/python-lsp-server/test/test_workspace.py @@ -69,6 +69,45 @@ def test_root_project_with_no_setup_py(pylsp): assert workspace_root in test_doc.sys_path() +def test_multiple_workspaces_from_initialize(pylsp_w_workspace_folders): + pylsp, workspace_folders = pylsp_w_workspace_folders + + assert len(pylsp.workspaces) == 2 + + folders_uris = [uris.from_fs_path(str(folder)) for folder in workspace_folders] + + for folder_uri in folders_uris: + assert folder_uri in pylsp.workspaces + + assert folders_uris[0] == pylsp.root_uri + + # Create file in the first workspace folder. + file1 = workspace_folders[0].join('file1.py') + file1.write('import os') + msg1 = { + 'uri': path_as_uri(str(file1)), + 'version': 1, + 'text': 'import os' + } + + pylsp.m_text_document__did_open(textDocument=msg1) + assert msg1['uri'] in pylsp.workspace._docs + assert msg1['uri'] in pylsp.workspaces[folders_uris[0]]._docs + + # Create file in the second workspace folder. + file2 = workspace_folders[1].join('file2.py') + file2.write('import sys') + msg2 = { + 'uri': path_as_uri(str(file2)), + 'version': 1, + 'text': 'import sys' + } + + pylsp.m_text_document__did_open(textDocument=msg2) + assert msg2['uri'] not in pylsp.workspace._docs + assert msg2['uri'] in pylsp.workspaces[folders_uris[1]]._docs + + def test_multiple_workspaces(tmpdir, pylsp): workspace1_dir = tmpdir.mkdir('workspace1') workspace2_dir = tmpdir.mkdir('workspace2') diff --git a/spyder/plugins/completion/providers/languageserver/providers/document.py b/spyder/plugins/completion/providers/languageserver/providers/document.py index 702379d9ea9..9c609e3e965 100644 --- a/spyder/plugins/completion/providers/languageserver/providers/document.py +++ b/spyder/plugins/completion/providers/languageserver/providers/document.py @@ -92,6 +92,9 @@ def document_completion_request(self, params): def process_document_completion(self, response, req_id): if isinstance(response, dict): response = response['items'] + + must_resolve = self.server_capabilites['completionProvider'].get( + 'resolveProvider', False) if response is not None: for item in response: item['kind'] = item.get('kind', CompletionItemKind.TEXT) @@ -103,6 +106,7 @@ def process_document_completion(self, response, req_id): 'insertTextFormat', InsertTextFormat.PLAIN_TEXT) item['insertText'] = item.get('insertText', item['label']) item['provider'] = LSP_COMPLETION + item['resolve'] = must_resolve if req_id in self.req_reply: self.req_reply[req_id]( @@ -110,6 +114,28 @@ def process_document_completion(self, response, req_id): {'params': response} ) + @send_request(method=CompletionRequestTypes.COMPLETION_RESOLVE) + def completion_resolve_request(self, params): + return params['completion_item'] + + @handles(CompletionRequestTypes.COMPLETION_RESOLVE) + def handle_completion_resolve(self, response, req_id): + response['kind'] = response.get('kind', CompletionItemKind.TEXT) + response['detail'] = response.get('detail', '') + response['documentation'] = response.get('documentation', '') + response['sortText'] = response.get('sortText', response['label']) + response['filterText'] = response.get('filterText', response['label']) + response['insertTextFormat'] = response.get( + 'insertTextFormat', InsertTextFormat.PLAIN_TEXT) + response['insertText'] = response.get('insertText', response['label']) + response['provider'] = LSP_COMPLETION + + if req_id in self.req_reply: + self.req_reply[req_id]( + CompletionRequestTypes.COMPLETION_RESOLVE, + {'params': response} + ) + @send_request(method=CompletionRequestTypes.DOCUMENT_SIGNATURE) def signature_help_request(self, params): params = { diff --git a/spyder/plugins/editor/widgets/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor.py index 021c17b92d6..b88c4fc6b6e 100644 --- a/spyder/plugins/editor/widgets/codeeditor.py +++ b/spyder/plugins/editor/widgets/codeeditor.py @@ -519,6 +519,7 @@ def __init__(self, parent=None): self.will_save_until_notify = False self.enable_hover = True self.auto_completion_characters = [] + self.resolve_completions_enabled = False self.signature_completion_characters = [] self.go_to_definition_enabled = False self.find_references_enabled = False @@ -1057,6 +1058,8 @@ def register_completion_capabilities(self, capabilities): 'foldingRangeProvider', False) self.auto_completion_characters = ( completion_options['triggerCharacters']) + self.resolve_completions_enabled = ( + completion_options.get('resolveProvider', False)) self.signature_completion_characters = ( signature_options['triggerCharacters'] + ['=']) # FIXME: self.go_to_definition_enabled = capabilities['definitionProvider'] @@ -1419,6 +1422,17 @@ def sort_key(completion): except Exception: self.log_lsp_handle_errors('Error when processing completions') + @request(method=CompletionRequestTypes.COMPLETION_RESOLVE) + def resolve_completion_item(self, item): + return { + 'file': self.filename, + 'completion_item': item + } + + @handles(CompletionRequestTypes.COMPLETION_RESOLVE) + def handle_completion_item_resolution(self, response): + self.completion_widget.augment_completion_info(response['params']) + # ------------- LSP: Signature Hints ------------------------------------ @request(method=CompletionRequestTypes.DOCUMENT_SIGNATURE) def request_signature(self): diff --git a/spyder/plugins/editor/widgets/completion.py b/spyder/plugins/editor/widgets/completion.py index 0ac68528519..0a03f92c98c 100644 --- a/spyder/plugins/editor/widgets/completion.py +++ b/spyder/plugins/editor/widgets/completion.py @@ -71,6 +71,8 @@ def __init__(self, parent, ancestor): self.completion_list = None self.completion_position = None self.automatic = False + self.current_selected_item_label = None + self.current_selected_item_point = None self.display_index = [] # Setup item rendering @@ -95,6 +97,9 @@ def is_empty(self): def show_list(self, completion_list, position, automatic): """Show list corresponding to position.""" + self.current_selected_item_label = None + self.current_selected_item_point = None + if not completion_list: self.hide() return @@ -445,7 +450,25 @@ def item_selected(self, item=None): self.completion_position) self.hide() + def _get_insert_text(self, item): + if 'textEdit' in item: + insert_text = item['textEdit']['newText'] + else: + insert_text = item['insertText'] + + # Split by starting $ or language specific chars + chars = ['$'] + if self._language == 'python': + chars.append('(') + + for ch in chars: + insert_text = insert_text.split(ch)[0] + return insert_text + def trigger_completion_hint(self, row=None): + self.current_selected_item_label = None + self.current_selected_item_point = None + if not self.completion_list: return if row is None: @@ -457,18 +480,17 @@ def trigger_completion_hint(self, row=None): if 'point' not in item: return - if 'textEdit' in item: - insert_text = item['textEdit']['newText'] - else: - insert_text = item['insertText'] + self.current_selected_item_label = item['label'] + self.current_selected_item_point = item['point'] - # Split by starting $ or language specific chars - chars = ['$'] - if self._language == 'python': - chars.append('(') + insert_text = self._get_insert_text(item) - for ch in chars: - insert_text = insert_text.split(ch)[0] + if hasattr(self.textedit, 'resolve_completion_item'): + if item.get('resolve', False): + to_resolve = item.copy() + to_resolve.pop('point') + to_resolve.pop('resolve') + self.textedit.resolve_completion_item(to_resolve) if isinstance(item['documentation'], dict): item['documentation'] = item['documentation']['value'] @@ -478,6 +500,14 @@ def trigger_completion_hint(self, row=None): item['documentation'], item['point']) + def augment_completion_info(self, item): + if self.current_selected_item_label == item['label']: + insert_text = self._get_insert_text(item) + self.sig_completion_hint.emit( + insert_text, + item['documentation'], + self.current_selected_item_point) + @Slot(int) def row_changed(self, row): """Set completion hint info and show it."""