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..75439f51 --- /dev/null +++ b/CONFIGURATION.md @@ -0,0 +1,50 @@ +# 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_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..6da8e167 --- /dev/null +++ b/pylsp/config/schema.json @@ -0,0 +1,276 @@ +{ + "$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_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/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/scripts/jsonschema2md.py b/scripts/jsonschema2md.py new file mode 100644 index 00000000..b10de886 --- /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) 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