From 07f2bb80e2a6b5f1839940a5d9fc1e7fee825c66 Mon Sep 17 00:00:00 2001 From: krassowski Date: Fri, 9 Jul 2021 18:16:21 +0100 Subject: [PATCH 1/3] Restore the JSON schema, add human-readable configuration --- .github/workflows/static.yml | 8 +- CONFIGURATION.md | 51 +++++++ README.md | 10 +- pylsp/config/schema.json | 281 +++++++++++++++++++++++++++++++++++ scripts/jsonschema2md.py | 81 ++++++++++ 5 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 CONFIGURATION.md create mode 100644 pylsp/config/schema.json create mode 100644 scripts/jsonschema2md.py diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 04cd2a8f..d6aec6a1 100644 --- a/.github/workflows/static.yml +++ b/.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/CONFIGURATION.md b/CONFIGURATION.md new file mode 100644 index 00000000..b37380d6 --- /dev/null +++ b/CONFIGURATION.md @@ -0,0 +1,51 @@ +# 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.executable` | `string` | Language server executable | `"pylsp"` | +| `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_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.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/README.md b/README.md index 80fcfe03..aa427aba 100644 --- a/README.md +++ b/README.md @@ -60,11 +60,13 @@ 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` +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 @@ -86,6 +88,12 @@ To run the test suite: pip install .[test] && pytest ``` +After adding configuration options to `schema.json`, refresh the `CONFIGURATION.md` file with + +``` +python scripts/jsonschema2md.py pylsp/config/schema.json CONFIGURATION.md +``` + ## License This project is made available under the MIT License. diff --git a/pylsp/config/schema.json b/pylsp/config/schema.json new file mode 100644 index 00000000..2a8d0cb9 --- /dev/null +++ b/pylsp/config/schema.json @@ -0,0 +1,281 @@ +{ + "$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.executable": { + "type": "string", + "default": "pylsp", + "description": "Language server executable" + }, + "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_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.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/scripts/jsonschema2md.py b/scripts/jsonschema2md.py new file mode 100644 index 00000000..f9f99eae --- /dev/null +++ b/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) From 03cda4e69592d358a120dfb9d1f4b3ed9a6af395 Mon Sep 17 00:00:00 2001 From: krassowski Date: Fri, 9 Jul 2021 18:25:52 +0100 Subject: [PATCH 2/3] Remove pylsp.executable, it is no longer used It was used in the VS Code extension see 7e911dc5a2f93f7d07a076e63778822fa61daa21 --- CONFIGURATION.md | 1 - pylsp/config/schema.json | 5 ----- scripts/jsonschema2md.py | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index b37380d6..75439f51 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -3,7 +3,6 @@ This server can be configured using `workspace/didChangeConfiguration` method. E | **Configuration Key** | **Type** | **Description** | **Default** |----|----|----|----| -| `pylsp.executable` | `string` | Language server executable | `"pylsp"` | | `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` | diff --git a/pylsp/config/schema.json b/pylsp/config/schema.json index 2a8d0cb9..6da8e167 100644 --- a/pylsp/config/schema.json +++ b/pylsp/config/schema.json @@ -4,11 +4,6 @@ "description": "This server can be configured using `workspace/didChangeConfiguration` method. Each configuration option is described below:", "type": "object", "properties": { - "pylsp.executable": { - "type": "string", - "default": "pylsp", - "description": "Language server executable" - }, "pylsp.configurationSources": { "type": "array", "default": ["pycodestyle"], diff --git a/scripts/jsonschema2md.py b/scripts/jsonschema2md.py index f9f99eae..b10de886 100644 --- a/scripts/jsonschema2md.py +++ b/scripts/jsonschema2md.py @@ -62,7 +62,7 @@ def convert_schema(schema: dict, source: str = None) -> str: ) # ensure empty line at the end - lines.append('') + lines.append("") return "\n".join(lines) From d08cd545ad7115b8303bb647333e67fd6ed4b280 Mon Sep 17 00:00:00 2001 From: krassowski Date: Fri, 9 Jul 2021 18:30:44 +0100 Subject: [PATCH 3/3] Update style to match new pylint suggestions --- pylsp/plugins/jedi_completion.py | 2 +- pylsp/python_lsp.py | 6 ++---- test/test_utils.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pylsp/plugins/jedi_completion.py b/pylsp/plugins/jedi_completion.py index 5b07f56b..a04d2101 100644 --- a/pylsp/plugins/jedi_completion.py +++ b/pylsp/plugins/jedi_completion.py @@ -114,7 +114,7 @@ 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 diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index f15ccd66..81efc353 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -363,8 +363,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) @@ -433,8 +432,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/test/test_utils.py b/test/test_utils.py index 61e4f1f5..4b41155b 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -2,8 +2,8 @@ # 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